-
Notifications
You must be signed in to change notification settings - Fork 471
feat: add mpp-vscode support #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Create project directory structure - Add package.json with VSCode extension configuration - Add tsconfig.json for TypeScript - Add README.md with TODO list and architecture overview Closes #31
Phase 2 - Core Services: - Add mpp-core bridge layer with LLMService, CodingAgent, ToolRegistry, CompletionManager, and DevInsCompiler wrappers - Implement extension.ts entry point with activation/deactivation Phase 3 - IDE Integration: - Implement IDE Server with Express HTTP endpoints - Add DiffManager for file diff operations - Support MCP protocol communication Phase 4 - Chat Interface: - Implement ChatViewProvider with webview - Add streaming LLM response support - Create embedded HTML chat UI Phase 5 - Commands: - Register autodev.chat, autodev.acceptDiff, autodev.cancelDiff, autodev.runAgent - Add keyboard shortcut (Cmd+Shift+A) Testing: - Add vitest configuration - Add unit tests for mpp-core bridge - Add unit tests for DiffManager - Add unit tests for IDEServer Refs #31
Phase 5 - Status Bar: - Add StatusBarManager with idle/thinking/streaming/error states - Animated status icons during LLM operations - Click to open chat command Phase 6 - DevIns Language Support: - Add TextMate grammar for syntax highlighting - Support commands (/), agents (@), variables ($) - Add language configuration (brackets, folding, etc.) - Implement DevInsCompletionProvider with built-in completions - Register completion triggers for /, @, $ characters Testing: - Add unit tests for StatusBarManager - Add unit tests for DevInsCompletionProvider - All 56 tests passing Refs #31
Phase 6 - React Webview UI: - Create webview React project with Vite build - Add MessageList component with Markdown rendering - Add ChatInput component with auto-resize textarea - Add useVSCode hook for extension communication - Integrate VSCode theme variables for consistent styling - Support streaming response with animated indicators - Add fallback inline HTML when React bundle not available - Update ChatViewProvider to load React bundle with CSP Components: - App.tsx - Main chat application - MessageList.tsx - Message display with Markdown support - ChatInput.tsx - Input with keyboard shortcuts - useVSCode.ts - VSCode API communication hook Testing: - Add ChatViewProvider tests - All 63 tests passing Refs #31
Extract MarkdownTextParser to a shared mpp-core module for reuse and improved testability across platforms. Update imports accordingly.
Phase 7 - Sketch Renderer System: - Add Timeline component mirroring mpp-ui's ComposeRenderer - Add SketchRenderer with specialized sub-renderers: - CodeBlockRenderer: syntax highlighting with Copy/Insert/Apply actions - DiffRenderer: diff display with Accept/Reject/View actions - ThinkingRenderer: collapsible thinking blocks - TerminalRenderer: terminal command and output display - MarkdownRenderer: GFM markdown with react-markdown - ToolCallRenderer: tool call information display - Add codeFence parser mirroring mpp-core's CodeFence.parseAll() - Add timeline types (TimelineItem, AgentState, ToolCallInfo, etc.) ChatViewProvider Refactoring: - Replace LLMService with CodingAgent for agent-based interactions - Add VSCodeRenderer implementing JsCodingAgentRenderer interface - Forward agent events (toolCall, toolResult, terminalOutput) to webview - Add action handlers (insert, apply, run-command, accept-diff, etc.) - Remove API key requirement - use helpful guidance for non-DevIns input All 63 tests passing. Refs #31
Phase 8 - Configuration Management Integration Changes: - Add ConfigManager service to load config from ~/.autodev/config.yaml - Refactor ChatViewProvider to use ConfigManager instead of VSCode settings - Mirror IdeaAgentViewModel's loadConfiguration() pattern - Add lazy initialization of CodingAgent with proper config validation - Create renderer that forwards events to webview (like TuiRenderer) - Add 'Open Config' button in header and config prompt - Support all LLM providers: openai, anthropic, deepseek, ollama, etc. - Auto-create config template when file doesn't exist Architecture: - ConfigManager.load() → AutoDevConfigWrapper → getActiveConfig() - isValid() check before agent initialization - JsKoogLLMService + JsCodingAgent from mpp-core - Renderer implements all JsCodingAgentRenderer methods All 63 tests passing. Refs #31
- Add ModelSelector component similar to IdeaModelSelector.kt - Update ChatInput with toolbar: ModelSelector on left, Stop/Send on right - Add config state management in App.tsx - Add sendConfigUpdate, stopExecution, selectConfig in chat-view.ts - Add new message types: configUpdate, stopExecution, selectConfig All 63 tests passing. Refs #31
- Acquire VSCode API in HTML before React loads to avoid duplicate acquisition error - Store API on window.vscodeApi for React to use - Remove debug logging from production code All 63 tests passing. Refs #31
User message was being added twice - once in handleSend for immediate feedback and again when receiving userMessage from extension. Refs #31
- Fix repository URL from phodal/auto-dev to phodal/auto-dev-sketch - Remove unused colors variable in status-bar.ts - Add platform detection for welcome message keybinding (Cmd vs Ctrl)
- Create project directory structure - Add package.json with VSCode extension configuration - Add tsconfig.json for TypeScript - Add README.md with TODO list and architecture overview Closes #31
Phase 2 - Core Services: - Add mpp-core bridge layer with LLMService, CodingAgent, ToolRegistry, CompletionManager, and DevInsCompiler wrappers - Implement extension.ts entry point with activation/deactivation Phase 3 - IDE Integration: - Implement IDE Server with Express HTTP endpoints - Add DiffManager for file diff operations - Support MCP protocol communication Phase 4 - Chat Interface: - Implement ChatViewProvider with webview - Add streaming LLM response support - Create embedded HTML chat UI Phase 5 - Commands: - Register autodev.chat, autodev.acceptDiff, autodev.cancelDiff, autodev.runAgent - Add keyboard shortcut (Cmd+Shift+A) Testing: - Add vitest configuration - Add unit tests for mpp-core bridge - Add unit tests for DiffManager - Add unit tests for IDEServer Refs #31
Phase 5 - Status Bar: - Add StatusBarManager with idle/thinking/streaming/error states - Animated status icons during LLM operations - Click to open chat command Phase 6 - DevIns Language Support: - Add TextMate grammar for syntax highlighting - Support commands (/), agents (@), variables ($) - Add language configuration (brackets, folding, etc.) - Implement DevInsCompletionProvider with built-in completions - Register completion triggers for /, @, $ characters Testing: - Add unit tests for StatusBarManager - Add unit tests for DevInsCompletionProvider - All 56 tests passing Refs #31
Phase 6 - React Webview UI: - Create webview React project with Vite build - Add MessageList component with Markdown rendering - Add ChatInput component with auto-resize textarea - Add useVSCode hook for extension communication - Integrate VSCode theme variables for consistent styling - Support streaming response with animated indicators - Add fallback inline HTML when React bundle not available - Update ChatViewProvider to load React bundle with CSP Components: - App.tsx - Main chat application - MessageList.tsx - Message display with Markdown support - ChatInput.tsx - Input with keyboard shortcuts - useVSCode.ts - VSCode API communication hook Testing: - Add ChatViewProvider tests - All 63 tests passing Refs #31
Extract MarkdownTextParser to a shared mpp-core module for reuse and improved testability across platforms. Update imports accordingly.
Phase 7 - Sketch Renderer System: - Add Timeline component mirroring mpp-ui's ComposeRenderer - Add SketchRenderer with specialized sub-renderers: - CodeBlockRenderer: syntax highlighting with Copy/Insert/Apply actions - DiffRenderer: diff display with Accept/Reject/View actions - ThinkingRenderer: collapsible thinking blocks - TerminalRenderer: terminal command and output display - MarkdownRenderer: GFM markdown with react-markdown - ToolCallRenderer: tool call information display - Add codeFence parser mirroring mpp-core's CodeFence.parseAll() - Add timeline types (TimelineItem, AgentState, ToolCallInfo, etc.) ChatViewProvider Refactoring: - Replace LLMService with CodingAgent for agent-based interactions - Add VSCodeRenderer implementing JsCodingAgentRenderer interface - Forward agent events (toolCall, toolResult, terminalOutput) to webview - Add action handlers (insert, apply, run-command, accept-diff, etc.) - Remove API key requirement - use helpful guidance for non-DevIns input All 63 tests passing. Refs #31
Phase 8 - Configuration Management Integration Changes: - Add ConfigManager service to load config from ~/.autodev/config.yaml - Refactor ChatViewProvider to use ConfigManager instead of VSCode settings - Mirror IdeaAgentViewModel's loadConfiguration() pattern - Add lazy initialization of CodingAgent with proper config validation - Create renderer that forwards events to webview (like TuiRenderer) - Add 'Open Config' button in header and config prompt - Support all LLM providers: openai, anthropic, deepseek, ollama, etc. - Auto-create config template when file doesn't exist Architecture: - ConfigManager.load() → AutoDevConfigWrapper → getActiveConfig() - isValid() check before agent initialization - JsKoogLLMService + JsCodingAgent from mpp-core - Renderer implements all JsCodingAgentRenderer methods All 63 tests passing. Refs #31
- Add ModelSelector component similar to IdeaModelSelector.kt - Update ChatInput with toolbar: ModelSelector on left, Stop/Send on right - Add config state management in App.tsx - Add sendConfigUpdate, stopExecution, selectConfig in chat-view.ts - Add new message types: configUpdate, stopExecution, selectConfig All 63 tests passing. Refs #31
- Acquire VSCode API in HTML before React loads to avoid duplicate acquisition error - Store API on window.vscodeApi for React to use - Remove debug logging from production code All 63 tests passing. Refs #31
User message was being added twice - once in handleSend for immediate feedback and again when receiving userMessage from extension. Refs #31
- Fix repository URL from phodal/auto-dev to phodal/auto-dev-sketch - Remove unused colors variable in status-bar.ts - Add platform detection for welcome message keybinding (Cmd vs Ctrl)
- Use mpp-core's JsCompletionManager for DevIn command completions - Add getCompletions and applyCompletion message handlers - Update CompletionPopup to support mpp-core CompletionItem format - Fix popup positioning by removing overflow:hidden from container - Add completion state management in App.tsx
feat(vscode): Enhanced Chat Input with DevIn Language Support
- Add SelectedFileItem, FileChip, TopToolbar, FileSearchPopup components - Add WorkspaceFileSearchProvider with pre-built file index for fast search - Add IndexingState enum for tracking indexing progress - Integrate file context into DevInEditorInput with buildAndSendMessage() - Add Prompt Enhancement button to BottomToolbar - Add AutoAwesome and History icons to AutoDevComposeIcons - Add FileContext to EditorCallbacks for file context submission Closes #35
* feat(mpp-ui): implement file context management with indexed search - Add SelectedFileItem, FileChip, TopToolbar, FileSearchPopup components - Add WorkspaceFileSearchProvider with pre-built file index for fast search - Add IndexingState enum for tracking indexing progress - Integrate file context into DevInEditorInput with buildAndSendMessage() - Add Prompt Enhancement button to BottomToolbar - Add AutoAwesome and History icons to AutoDevComposeIcons - Add FileContext to EditorCallbacks for file context submission Closes #35 * feat(mpp-core): add plan management data models and parser for task management - Add TaskStatus enum (TODO, IN_PROGRESS, COMPLETED, FAILED, BLOCKED) and PlanPhase enum (PDCA cycle) - Add CodeFileLink data class for markdown file link extraction - Add PlanStep, PlanTask, AgentPlan data models with serialization support - Add MarkdownPlanParser using pure Kotlin regex (no IntelliJ dependencies) - Add PlanStateService with StateFlow and listener pattern for reactive updates - Add PlanUpdateListener interface for UI notifications - Add comprehensive unit tests for parser and state service - Fix DocQLReturnAllTest missing Pending branch in when expressions Part of #37 * feat(mpp-core): add PlanManagementTool for AI agent task planning - Add PlanManagementTool with CREATE, UPDATE, COMPLETE_STEP, FAIL_STEP, VIEW actions - Add PlanManagementParams, PlanManagementSchema, PlanManagementInvocation - Add comprehensive unit tests for all tool actions - Tool integrates with PlanStateService for state management Part of #37 * feat(mpp-core): register PlanManagementTool in BuiltinToolsProvider - Add PlanManagementTool to BuiltinToolsProvider.provide() - Add executePlanManagementTool method in ToolOrchestrator - Add tests for plan and task-boundary tool registration Part of #37 * feat(mpp-ui): add PlanPanel UI component and integrate with ComposeRenderer - Create PlanPanel composable with task and step display - Add expandable task cards with progress tracking - Implement status icons and colors for plan visualization - Integrate plan state tracking in ComposeRenderer - Handle plan tool calls to update UI state Part of #37 * feat(mpp-core): add plan management guidance to system prompt and fix parameter parsing - Add Planning and Task Management section to CodingAgentTemplate (EN and ZH) - Document when to use planning, plan format, and plan actions - Update Task Completion Strategy to include planning step - Fix taskIndex/stepIndex parameter parsing in ToolOrchestrator to handle both Number and String types Part of #37 * refactor(mpp-core): remove TaskBoundaryTool in favor of PlanManagementTool - Remove TaskBoundaryTool.kt as PlanManagementTool provides superset functionality - Remove TaskBoundaryTool from BuiltinToolsProvider and ToolOrchestrator - Update ToolRegistryTest to remove task-boundary test - Update comments referencing task-boundary - Enhance PlanManagementTool with detailed KDoc documentation - Fix tuiEmoji to use proper emoji character Part of #37 * fix(executor): reduce shell command max timeout to 2 minutes Lower the maximum wait time for shell command execution to 2 minutes and pass it as a timeout to the orchestrator context.
WalkthroughIntroduces a comprehensive plan management system replacing task-boundary tracking, with new data models (AgentPlan, PlanTask, PlanStep), a plan-aware tool (PlanManagementTool), state service with listeners, markdown parsing, UI components, file search capabilities, and a complete VSCode extension with chat webview, IDE server integration, and diff management. Changes
Sequence DiagramsequenceDiagram
actor User
participant VSCode as VSCode<br/>Extension
participant Agent as Coding<br/>Agent
participant PlanSvc as Plan State<br/>Service
participant UI as Webview<br/>UI
User->>VSCode: Submit task
VSCode->>Agent: execute(input)
Agent->>Agent: Process & invoke tool
Agent->>VSCode: Call plan tool<br/>(CREATE)
VSCode->>PlanSvc: createPlanFromMarkdown()
PlanSvc->>PlanSvc: Parse, create AgentPlan<br/>notify listeners
PlanSvc-->>VSCode: AgentPlan
VSCode->>UI: Render plan
UI->>User: Display tasks/steps
loop Execute Plan Steps
Agent->>Agent: Execute step
Agent->>VSCode: Call plan tool<br/>(COMPLETE_STEP)
VSCode->>PlanSvc: completeStep(taskId, stepId)
PlanSvc->>PlanSvc: Update step status,<br/>refresh plan
PlanSvc-->>VSCode: Updated plan
VSCode->>UI: Re-render with progress
UI->>User: Show step completed
end
Agent->>VSCode: Execution complete
VSCode->>UI: Render final result
UI->>User: Display summary
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Specific areas requiring attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
@copilot 修复 merge 问题 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive VSCode extension support for AutoDev, implementing a feature-rich coding assistant based on Kotlin Multiplatform technology. The extension includes a React-based webview UI, backend services for diff management and IDE server communication, and integration with the mpp-core Kotlin/JS module.
Key Changes:
- React + TypeScript webview with Timeline-based chat architecture
- VSCode extension services (diff management, IDE server, status bar)
- Bridge to mpp-core for LLM services, completion management, and coding agent
- DevIns language support with syntax highlighting and auto-completion
Reviewed changes
Copilot reviewed 104 out of 105 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| mpp-vscode/package.json | Main extension manifest with commands, views, and configuration |
| mpp-vscode/webview/package.json | Webview dependencies (React, Vite, TypeScript) |
| mpp-vscode/tsconfig.json | TypeScript configuration for extension |
| mpp-vscode/webview/tsconfig.json | TypeScript configuration for webview |
| mpp-vscode/webview/vite.config.ts | Vite build configuration for webview |
| mpp-vscode/vitest.config.ts | Vitest test configuration |
| mpp-vscode/src/extension.ts | Extension entry point with activation logic |
| mpp-vscode/src/bridge/mpp-core.ts | TypeScript bridge to Kotlin/JS mpp-core module |
| mpp-vscode/src/services/*.ts | IDE server, diff manager, status bar, config manager |
| mpp-vscode/src/providers/*.ts | Chat view provider and DevIns completion provider |
| mpp-vscode/webview/src/App.tsx | Main React application component |
| mpp-vscode/webview/src/components/*.tsx | React UI components (Timeline, ChatInput, ModelSelector, etc.) |
| mpp-vscode/webview/src/components/sketch/*.tsx | Specialized renderers for different content types |
| mpp-vscode/test/**/*.test.ts | Unit tests for services and providers |
| mpp-vscode/syntaxes/*.json | DevIns language syntax definitions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| thinking: 'Thinking', | ||
| walkthrough: 'Walkthrough', | ||
| mermaid: 'Mermaid', | ||
| devin: 'DevIn', |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The display name 'DevIn' has inconsistent capitalization compared to other entries. Should this be 'Devin' to match the typical naming pattern?
|
|
||
| interface ToolCallRendererProps { | ||
| toolCall: ToolCallInfo; | ||
| onAction?: (action: string, data: any) => void; |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing type annotation for the any parameter in onAction. Consider using a more specific type:
onAction?: (action: string, data: Record<string, unknown>) => void;| interface SketchRendererProps { | ||
| content: string; | ||
| isComplete?: boolean; | ||
| onAction?: (action: string, data: any) => void; |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The any type is used multiple times for action data parameters. Consider defining a union type for all possible actions:
type ActionData =
| { action: 'rerun-tool'; data: { toolName: string; params: string } }
| { action: 'accept-diff'; data: { diff: string } }
| { action: 'run-command'; data: { command: string } }
// ... other actions| border: 1px solid var(--vscode-panel-border); | ||
| border-radius: 6px; | ||
| overflow: hidden; | ||
| background-color: var(--vscode-editorWidget-background); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] CSS custom properties are used without fallback values in several places. While VSCode themes should provide these, consider adding fallback values for better robustness:
background: var(--vscode-editorWidget-background, #252526);| ref={highlightRef} | ||
| className="devin-highlight-overlay" | ||
| dangerouslySetInnerHTML={{ __html: getHighlightedHtml() }} |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: dangerouslySetInnerHTML is used with user input. While escapeHtml function is used to sanitize, ensure this covers all XSS attack vectors. The current implementation looks correct, but consider using a well-tested library like DOMPurify for additional security.
| JsCompletionManager, | ||
| JsDevInsCompiler, | ||
| JsToolRegistry, | ||
| JsCompressionConfig |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable JsCompressionConfig.
| return; | ||
| } | ||
|
|
||
| const basePath = workspaceFolders[0].uri.fsPath; |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable basePath.
| const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(webviewPath, 'assets', 'index.css')); | ||
|
|
||
| // Use nonce for security | ||
| const nonce = this.getNonce(); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable nonce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 19
🧹 Nitpick comments (78)
mpp-vscode/test/bridge/mpp-core.test.ts (3)
104-113: Test doesn't verify meaningful clearing behavior.The test calls
clearHistory()on a service that already has empty history, then asserts it's empty. This doesn't proveclearHistoryactually clears anything.Consider populating history before clearing:
it('should clear history', async () => { const { LLMService } = await import('../../src/bridge/mpp-core'); const service = new LLMService({ provider: 'openai', model: 'gpt-4', apiKey: 'test-key' }); + // Populate history first by calling streamMessage or directly + await service.streamMessage('test message', () => {}); + expect(service.getHistory().length).toBeGreaterThan(0); service.clearHistory(); expect(service.getHistory()).toEqual([]); });
129-134: Missing instantiation test for consistency.Other classes like
CompletionManagerandToolRegistryhave an instantiation test. Consider adding one here for consistency:describe('DevInsCompiler', () => { it('should be importable', async () => { const { DevInsCompiler } = await import('../../src/bridge/mpp-core'); expect(DevInsCompiler).toBeDefined(); }); + + it('should create instance', async () => { + const { DevInsCompiler } = await import('../../src/bridge/mpp-core'); + const compiler = new DevInsCompiler(); + expect(compiler).toBeDefined(); + }); });
156-168: Consider verifying actual mock return values.The mock returns specific values (
['gpt-4', 'gpt-3.5-turbo']and['OPENAI', 'ANTHROPIC']), but the tests only verify the return type is an array. Stronger assertions would catch issues with the bridge layer's data transformation:it('should get available models', async () => { const { getAvailableModels } = await import('../../src/bridge/mpp-core'); const models = getAvailableModels('openai'); expect(Array.isArray(models)).toBe(true); + expect(models).toContain('gpt-4'); + expect(models).toContain('gpt-3.5-turbo'); }); it('should get all providers', async () => { const { getAllProviders } = await import('../../src/bridge/mpp-core'); const providers = getAllProviders(); expect(Array.isArray(providers)).toBe(true); + expect(providers).toContain('OPENAI'); + expect(providers).toContain('ANTHROPIC'); });mpp-vscode/webview/src/components/MessageList.tsx (4)
74-78: Prefer a stable key over array index for messagesUsing
indexas the React key can cause subtle UI glitches if messages are ever inserted/removed anywhere but the end of the list. Consider adding a stable identifier toMessage(e.g.,id: string) and using that as the key.
50-55: Auto-scroll may override user scrolling historyUnconditionally setting
scrollTop = scrollHeighton everymessageschange will snap the view to the bottom even when the user has scrolled up to read older messages. Consider only auto-scrolling when the user is already near the bottom, or when the newest message is from the assistant.
64-68: Suggestions look clickable but are inert and lack semanticsThe suggestions are styled as clickable pills but rendered as plain
<span>elements with no click handlers or keyboard accessibility. Consider:
- Wiring them to callbacks (e.g.,
onSuggestionClick) and using<button>elements, or- At least adding
role="button"andtabIndex={0}plus keyboard handlers if you keep spans.
26-32: Add an accessible cue for the streaming indicatorThe three-dot streaming indicator is purely visual. For screen readers, consider adding an
aria-labelor visually hidden text (e.g., “AutoDev is responding…”) tied to the same conditional so assistive tech users get equivalent feedback.mpp-vscode/webview/src/components/sketch/DiffRenderer.css (1)
1-129: Styles are solid; consider routing remaining hardcoded colors through shared tokensThe diff viewer styling (container, actions, states, applied overlay) is coherent and leans on VSCode theming tokens, which is good. If you want stricter design‑token discipline later, consider moving the remaining raw colors (e.g.
#45a049,#da190b,whiteand the hex fallbacks) into shared CSS variables so they’re easier to tweak across components, but this is non‑blocking.mpp-vscode/webview/src/components/sketch/TerminalRenderer.css (1)
34-42: Avoid hardcodedcolor: whitefor status badges.Per coding guidelines, hardcoded colors should be avoided. Use a CSS variable with fallback for consistency with the rest of the file.
.terminal-status.success { background-color: var(--vscode-testing-iconPassed, #4caf50); - color: white; + color: var(--vscode-button-foreground, #ffffff); } .terminal-status.error { background-color: var(--vscode-testing-iconFailed, #f44336); - color: white; + color: var(--vscode-button-foreground, #ffffff); }mpp-vscode/webview/src/components/ChatInput.css (1)
47-50: Potential low-contrast hover state.The
.toolbar-button:hoveruses--selection-backgroundwhich may already be the same as the surrounding toolbar background (.input-toolbaralso uses--selection-background). This could result in no visible hover effect beyond the opacity change.Consider using a different variable or adding a subtle differentiation:
.toolbar-button:hover:not(:disabled) { opacity: 1; - background: var(--selection-background); + background: var(--list-hover-background, var(--selection-background)); }mpp-vscode/webview/src/hooks/useVSCode.ts (1)
104-111: Consider validating message structure in onMessage handler.The
onMessagehandler castsevent.datadirectly toExtensionMessagewithout validation. Malicious or malformed messages could cause runtime errors. Consider adding a type guard.const onMessage = useCallback((handler: (message: ExtensionMessage) => void) => { const listener = (event: MessageEvent<ExtensionMessage>) => { + // Validate message has expected structure + if (event.data && typeof event.data.type === 'string') { handler(event.data); + } }; window.addEventListener('message', listener); return () => window.removeEventListener('message', listener); }, []);mpp-vscode/src/bridge/mpp-core.ts (1)
440-449: Minor code duplication: model config creation.The
JsModelConfigcreation logic at lines 441-449 duplicates lines 64-73 inLLMService. Consider extracting a helper function for consistency.// Add helper function at module level function createJsModelConfig(config: ModelConfig): any { const providerName = ProviderTypes[config.provider.toLowerCase()] || config.provider.toUpperCase(); return new JsModelConfig( providerName, config.model, config.apiKey, config.temperature ?? 0.7, config.maxTokens ?? 8192, config.baseUrl ?? '' ); }mpp-vscode/README.md (1)
108-111: Avoid hardcoded local filesystem paths in docsThe build step uses a machine-specific path:
cd /Volumes/source/ai/autocrudConsider switching to a repo-relative or placeholder path (e.g.,
cd /path/to/autocrudor from the monorepo root) so other contributors can follow the instructions directly.mpp-vscode/test/providers/chat-view.test.ts (2)
5-91: Good structural tests; consider adding behavior assertions for message flowThe suite validates construction and public method presence, but doesn’t assert behavior (e.g., that
sendMessageusesLLMService.streamMessageor that errors/history clear are posted back to the webview). Adding a couple of focused tests around these flows would better protect the extension’s core chat UX while still using your existing mocks.
94-141: Private HTML helper coverage is solid; optional: assert asset URI wiringThe HTML tests nicely cover CSP, root container, and fallback inline UI markers via the private
getHtmlContent. If you want slightly stronger regression protection, you could also assert thatwebview.asWebviewUriis called with the expecteddist/webviewasset paths so bundle layout changes don’t silently break the React path.mpp-vscode/webview/src/components/sketch/ThinkingRenderer.tsx (1)
9-59: Component logic looks good; consider exporting the props type
ThinkingRendereris clean and satisfies the guidelines (no inline colors, CSS-based styling). If you plan to reuse the props type elsewhere (e.g., in a higher-level sketch renderer API), consider exportingThinkingRendererPropsso consumers can import a single source of truth for the props shape.mpp-vscode/test/services/status-bar.test.ts (1)
7-98: Solid smoke tests; consider fake timers for time-based behaviorThe tests do a good job ensuring
StatusBarManagerconstructs and its public methods don’t throw against mocked vscode APIs. To harden against regressions in the animation and temporary-message logic, consider using vitest fake timers to assert thatshowMessageeventually restores the original text and thatsetState('thinking'/'streaming')starts (anddisposestops) the animation interval.mpp-vscode/webview/src/components/sketch/ToolCallRenderer.tsx (1)
10-126: Renderer is well-structured; consider using fullParams for rerun/formattingThe renderer cleanly maps
successto icon/CSS state and safely parsesparamswith a fallback. SinceToolCallInfoalso exposesfullParams/fullOutput, consider preferringfullParams ?? paramsforformatParamsand for the rerun payload, and optionallyfullOutput ?? outputfor display. That would keep the UI concise while ensuring reruns and detailed views use the complete data rather than any truncated variant.mpp-vscode/webview/src/components/sketch/CodeBlockRenderer.tsx (1)
9-87: Code block UX is good; tighten action typing and consider a clipboard fallbackThe component provides a nice “copy / insert / apply” UX and follows the styling guideline (no inline colors). Two optional improvements:
- Narrow
onActionto a string union like'insert' | 'apply'with a typed payload to avoid accidental misuse from callers.- For environments where
navigator.clipboardis unavailable or denied, consider invokingonAction?.('insert', …)or surfacing a small inline error instead of only logging to the console so users still have a way to get the code.mpp-vscode/test/services/ide-server.test.ts (1)
90-124: Endpoint describe block is effectively a placeholderThe “IDEServer API Endpoints” tests only assert that
IDEServeris defined, so they won’t fail even if all HTTP handlers are removed. Either extend these to call the handlers (in an integration-style suite) or drop/rename this block so expectations match what’s actually tested.mpp-vscode/webview/src/components/CompletionPopup.css (1)
27-34: Avoid redundant.completion-item.selectedbackground rule
.completion-item:hover, .completion-item.selectedand the later.completion-item.selectedboth setbackground. Because the latter wins, the.completion-item.selectedpart of the first rule is redundant; you can drop it to make the cascade clearer.mpp-vscode/webview/src/components/FileSearchPopup.css (1)
89-96: Tidy up duplicated.file-item.selectedbackground rulesYou set
backgroundfor.file-item:hover, .file-item.selectedand then override it in a later.file-item.selectedrule. The first rule could just be.file-item:hoverwithout changing behavior.mpp-vscode/test/providers/devins-completion.test.ts (1)
5-143: Good coverage of trigger characters; consider asserting item shapeThe tests nicely cover import/instantiation and that
/,@,$prefixes yield non-empty completion arrays while regular text yields none. To catch regressions more precisely, you might additionally assert a couple of properties on returned items (e.g.,labelformat orkind) so changes in built‑in lists or matching logic are surfaced earlier.mpp-vscode/test/services/diff-manager.test.ts (1)
140-157: “should emit events on diff changes” test never triggers a changeIn the DiffManager suite you attach a listener and assert that
eventsis empty, but you never invoke any method (showDiff,acceptDiff,cancelDiff, etc.) that would fireonDidChange. As written, the test doesn’t validate the event pipeline. Either invoke a diff‑changing method and assert the emitted events, or rename/remove the test to match its behavior.mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/CodeFileLink.kt (1)
28-41: Regex is simple and works, but may over/under‑match in edge cases
LINK_PATTERNwill happily treat any markdown link (includinghttps://...) as aCodeFileLink, and it won’t handle file paths containing)characters. That’s fine if your plan steps only contain simple[label](relative/path.ext)links, but if you expect more varied markdown, consider tightening the pattern (e.g., to prefer path‑like targets) or documenting this limitation explicitly.mpp-vscode/webview/src/components/sketch/TerminalRenderer.tsx (1)
25-39: Handle copy timeout cleanup to avoid stale state updates
handleCopysets a 2s timeout that callssetCopied(false)but doesn’t clear that timeout if the component unmounts in the meantime, which can lead to “setState on unmounted component” warnings. Consider storing the timeout ID in a ref and clearing it in a cleanup (or using an effect tied tocopied) so the timer can’t fire after unmount.Also applies to: 60-75, 103-113
mpp-vscode/webview/src/components/DevInInput.tsx (2)
11-18: ExportDevInInputPropsif it’s part of the public surfaceIf callers outside this module are expected to type the component props (as implied by the summary), declare it as:
export interface DevInInputProps { value: string; onChange: (value: string) => void; onKeyDown?: (e: KeyboardEvent<HTMLTextAreaElement>) => void; onTriggerCompletion?: (trigger: '/' | '@' | '$', position: number) => void; placeholder?: string; disabled?: boolean; }Otherwise, keeping it internal is fine.
69-87:dangerouslySetInnerHTMLusage is XSS-safe as written; consider documenting thisStatic analysis warns about
dangerouslySetInnerHTML, but here all user input goes throughescapeHtmlbefore pattern highlighting, and the replacements only wrap the already-escaped text in<span>tags. That prevents script injection.To avoid regressions, consider adding a short comment near
getHighlightedHtmlnoting that:
- All user content must be passed through
escapeHtmlfirst.- Highlighting must never insert raw user strings into the HTML template.
No code change is strictly required.
Also applies to: 93-96
mpp-core/src/commonTest/kotlin/cc/unitmesh/agent/plan/MarkdownPlanParserTest.kt (1)
65-75: Align “checkmark symbol” test with the✓markerThe test name suggests covering the
✓marker, but the sample uses[x], which is already exercised inshould parse step status markers correctly. To better match the documentation inTaskStatusand strengthen coverage, consider changing the step to:val markdown = """ 1. Task with checkmark - [✓] Completed with checkmark """.trimIndent()So that both
xand✓are explicitly tested.mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/TaskStatus.kt (1)
22-44: Consider makingtoMarkeran instance method onTaskStatusThe logic is sound, but declaring
fun TaskStatus.toMarker()inside the companion is a bit unconventional and requires importing the companion’s extension. Defining it as an instance method keeps usage simple:@Serializable enum class TaskStatus { TODO, IN_PROGRESS, COMPLETED, FAILED, BLOCKED; fun toMarker(): String = when (this) { COMPLETED -> "✓" FAILED -> "!" IN_PROGRESS -> "*" TODO -> " " BLOCKED -> "B" } companion object { fun fromMarker(marker: String): TaskStatus = when (marker.trim().lowercase()) { "x", "✓" -> COMPLETED "!" -> FAILED "*" -> IN_PROGRESS "", " " -> TODO else -> TODO } } }Not mandatory, but a bit more idiomatic.
mpp-vscode/src/extension.ts (2)
52-66: Return Promises from async diff commands so VS Code can observe failures
diffManager.acceptDiff/cancelDiffare asynchronous; the command handlers invoke them but don’t return the resulting Promise. This can hide rejections from VS Code and from callers.Consider returning the Promise from the handlers:
context.subscriptions.push( vscode.commands.registerCommand('autodev.acceptDiff', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; if (docUri && docUri.scheme === DIFF_SCHEME) { return diffManager.acceptDiff(docUri); } }), vscode.commands.registerCommand('autodev.cancelDiff', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; if (docUri && docUri.scheme === DIFF_SCHEME) { return diffManager.cancelDiff(docUri); } }) );This lets VS Code handle errors and completion correctly.
15-41: Watch for circular dependency betweenextension.tsanddiff-manager.tsaroundDIFF_SCHEME
extension.tsdefines and exportsDIFF_SCHEME, while also importingDiffManager/DiffContentProviderfrom./services/diff-manager. Ifdiff-manager.tsin turn importsDIFF_SCHEMEfrom this file and uses it during module initialization (rather than only inside functions), that can lead to fragile circular import behavior.If that pattern exists, consider moving
DIFF_SCHEMEinto a small shared module (e.g.,./constants) imported by bothextension.tsanddiff-manager.ts, so neither file depends on the other at load time.mpp-vscode/webview/src/utils/codeFence.ts (1)
50-51: Language ID regex doesn't match all valid identifiers.The regex
\w+only matches word characters (letters, digits, underscores) but language identifiers likec++orc#won't be captured correctly.- const langMatch = trimmedLine.match(/^```(\w+)?/); + const langMatch = trimmedLine.match(/^```([a-zA-Z0-9+#_-]+)?/);mpp-vscode/src/providers/devins-completion.ts (1)
53-61: Remove or use thecompletionManagerfield.The
completionManageris instantiated but never used inprovideCompletionItemsor any helper method. This is dead code that should either be removed or integrated.If dynamic completions are not needed yet, remove the unused field:
export class DevInsCompletionProvider implements vscode.CompletionItemProvider { - private completionManager: CompletionManager | undefined; - - constructor() { - try { - this.completionManager = new CompletionManager(); - } catch (e) { - // mpp-core not available, use built-in completions only - } - } + // Built-in completions only; dynamic completions can be added laterOr implement the integration if intended.
mpp-vscode/src/services/config-manager.ts (2)
118-158: Consider using async file operations.The
load()method is markedasyncbut uses synchronousfs.existsSync,fs.mkdirSync, andfs.readFileSync. In a VSCode extension context, blocking the main thread during config load could degrade responsiveness.static async load(): Promise<AutoDevConfigWrapper> { try { - // Ensure directory exists - if (!fs.existsSync(this.CONFIG_DIR)) { - fs.mkdirSync(this.CONFIG_DIR, { recursive: true }); - } - - if (!fs.existsSync(this.CONFIG_FILE)) { - return this.createEmpty(); - } - - const content = fs.readFileSync(this.CONFIG_FILE, 'utf-8'); + // Ensure directory exists + await fs.promises.mkdir(this.CONFIG_DIR, { recursive: true }); + + try { + await fs.promises.access(this.CONFIG_FILE); + } catch { + return this.createEmpty(); + } + + const content = await fs.promises.readFile(this.CONFIG_FILE, 'utf-8'); const parsed = yaml.parse(content);
83-88: Missing MCP server validation in filter.The Kotlin implementation filters with
!it.value.disabled && it.value.validate(), but this implementation only checksdisabled. Invalid server configurations (e.g., missing bothcommandandurl) could be returned as enabled.Consider adding validation:
getEnabledMcpServers(): Record<string, McpServerConfig> { const mcpServers = this.getMcpServers(); return Object.fromEntries( - Object.entries(mcpServers).filter(([_, server]) => !server.disabled) + Object.entries(mcpServers).filter(([_, server]) => + !server.disabled && (server.command || server.url) + ) ); }mpp-vscode/webview/src/components/FileSearchPopup.tsx (1)
20-25: Consider reusingSelectedFileinterface from FileChip.tsx.
FileItemhas identical fields toSelectedFile(imported on line 10). This creates maintenance overhead if fields need to change.-interface FileItem { - name: string; - path: string; - relativePath: string; - isDirectory: boolean; -} +// Use SelectedFile from FileChip.tsx +type FileItem = SelectedFile;mpp-vscode/webview/src/components/FileChip.tsx (1)
42-46: Addaria-labelto remove button for accessibility.The button has a
titleattribute which provides a tooltip, butaria-labelis preferred for screen reader announcements.- <button className="file-chip-remove" onClick={onRemove} title="Remove from context"> + <button className="file-chip-remove" onClick={onRemove} title="Remove from context" aria-label={`Remove ${file.name} from context`}>mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt (1)
160-171: Consider adding click feedback for the remove button.The remove icon uses
clickablewithout an indication, which means no visual feedback on click. Consider adding a ripple or hover effect for better UX.Icon( imageVector = AutoDevComposeIcons.Close, contentDescription = "Remove from context", modifier = Modifier .size(16.dp) - .clickable(onClick = onRemove), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, // or use LocalIndication.current for ripple + onClick = onRemove + ), tint = if (isHovered) { MaterialTheme.colorScheme.onSurface } else { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) } )mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx (1)
42-46: Consider using a more stable key than index.Using
indexas a key can cause issues with React reconciliation if blocks are inserted or reordered during streaming. Consider generating a stable identifier, such as combining block position with a hash of the content prefix.{blocks.map((block, index) => { const isLastBlock = index === blocks.length - 1; const blockIsComplete = block.isComplete && (isComplete || !isLastBlock); return ( - <div key={index} className="sketch-block"> + <div key={`${index}-${block.languageId}-${block.text.slice(0, 20)}`} className="sketch-block"> {renderBlock(block, blockIsComplete, onAction)} </div> ); })}mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt (1)
241-246: Consider adding error logging for invalid plan markdown.When
planMarkdownis blank for CREATE/UPDATE actions, the code silently does nothing. Consider logging a warning for debugging purposes."CREATE", "UPDATE" -> { - if (planMarkdown.isNotBlank()) { - _currentPlan = MarkdownPlanParser.parseToPlan(planMarkdown) + if (planMarkdown.isNotBlank()) { + _currentPlan = MarkdownPlanParser.parseToPlan(planMarkdown) + } else { + // Consider logging: plan markdown is empty for $action } }mpp-vscode/webview/src/App.tsx (2)
119-137: Direct mutation of timeline item - potential React state issue.The
toolResulthandler mutatesitem.toolCalldirectly within the spread array. While this works because the outer array is spread, it's safer to create a new object for the modified item to avoid subtle React reconciliation issues.case 'toolResult': if (msg.data) { setAgentState(prev => { const timeline = [...prev.timeline]; // Find the last tool call and update it for (let i = timeline.length - 1; i >= 0; i--) { if (timeline[i].type === 'tool_call') { const item = timeline[i] as ToolCallTimelineItem; - item.toolCall = { - ...item.toolCall, - success: msg.data?.success as boolean | undefined, - output: msg.data?.output as string | undefined, - summary: msg.data?.summary as string | undefined - }; + timeline[i] = { + ...item, + toolCall: { + ...item.toolCall, + success: msg.data?.success as boolean | undefined, + output: msg.data?.output as string | undefined, + summary: msg.data?.summary as string | undefined + } + }; break; } } return { ...prev, timeline }; }); } break;
321-345: Potential memory leak in handlePromptOptimize.The event listener added via
window.addEventListenermay not be cleaned up if the component unmounts before the timeout fires or the response arrives. Consider usingAbortControlleror cleanup in auseEffect.const handlePromptOptimize = useCallback(async (prompt: string): Promise<string> => { return new Promise((resolve) => { postMessage({ type: 'action', action: 'optimizePrompt', data: { prompt } }); + let resolved = false; const handler = (event: MessageEvent) => { const msg = event.data; if (msg.type === 'promptOptimized' && msg.data?.optimizedPrompt) { + resolved = true; window.removeEventListener('message', handler); resolve(msg.data.optimizedPrompt as string); } else if (msg.type === 'promptOptimizeFailed') { + resolved = true; window.removeEventListener('message', handler); resolve(prompt); } }; window.addEventListener('message', handler); setTimeout(() => { - window.removeEventListener('message', handler); - resolve(prompt); + if (!resolved) { + window.removeEventListener('message', handler); + resolve(prompt); + } }, 30000); }); }, [postMessage]);mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/MarkdownPlanParser.kt (1)
113-119: Consider handling empty task list edge case.The
formatToMarkdownfunction works correctly but returns an empty string for an empty task list. This is probably fine, but worth documenting or handling explicitly if callers expect specific behavior./** * Format a list of tasks back to markdown. + * Returns an empty string if no tasks are provided. */ fun formatToMarkdown(tasks: List<PlanTask>): String { + if (tasks.isEmpty()) return "" val sb = StringBuilder() tasks.forEachIndexed { index, task -> sb.append(task.toMarkdown(index + 1)) } return sb.toString() }mpp-vscode/webview/src/components/sketch/DiffRenderer.tsx (1)
24-37: Consider memoizing parsed diff and handling CRLF/empty input more defensively
parseDiff(diffContent)runs on every render and assumes\nonly; mixed\r\ninput will leak\rintocontent, and large diffs may be slightly wasteful to re-parse on each re-render. You coulduseMemoaroundparseDiffand strip trailing\rper line (or normalize line endings) to keep rendering clean and predictable, especially when integrating with other tools that emit CRLF patches.Also applies to: 88-100
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt (1)
32-38:rememberforeffectiveSearchProvidershould takefileSearchProvideras a key
effectiveSearchProvideris created withremember { fileSearchProvider ?: DefaultFileSearchProvider }, so if a parent ever changesfileSearchProviderat runtime, this composable will keep using the initial instance. Preferremember(fileSearchProvider) { ... }so injected providers can be swapped predictably (tests, previews, different workspaces).mpp-vscode/webview/src/components/ModelSelector.tsx (1)
50-64: Optional: addtype="button"and basic ARIA hints for better accessibilitySince the trigger and list items are plain
<button>elements, addingtype="button"on the trigger andaria-haspopup="listbox"/aria-expanded={isOpen}would avoid accidental form submission and improve screen reader behavior without changing runtime logic.Also applies to: 66-108
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt (2)
108-110:rememberforeffectiveSearchProvidershould depend onfileSearchProvider
val effectiveSearchProvider = remember { fileSearchProvider ?: WorkspaceFileSearchProvider() }will ignore future changes tofileSearchProvider. If a different provider is ever passed (tests, alternate workspace), it won’t be picked up. Useremember(fileSearchProvider) { fileSearchProvider ?: WorkspaceFileSearchProvider() }to keep behavior intuitive.
111-139: Confirm that built-in commands should also carry file context and clear selections
buildAndSendMessagealways appends DevIns file commands forselectedFiles, callscallbacks?.onSubmit(fullText, getFileContexts()), then clearsselectedFiles. This path is now used for:
- Desktop Enter send
- Mobile IME send
- Bottom toolbar send
- Auto-executed completions for
/init,/clear,/helpIf the intent is that all messages, including these built-ins, should be contextual and also clear any selected files, this is fine. If some commands (e.g.
/clearor/help) should ignore file context and/or not reset selections, you may want separate helpers for “normal” messages vs. meta-commands.Also applies to: 315-316, 435-443, 553-560, 631-639
mpp-vscode/webview/src/components/CompletionPopup.tsx (1)
58-97: Be mindful of global keydown listener interactions with other componentsThe popup attaches a
window-levelkeydownlistener whileisOpen. This is fine, but if other parts of the webview also rely on global shortcuts, there’s a chance of conflicts. If this becomes an issue, you could:
- Restrict handling to specific keys and conditions (as you already do), and/or
- Gate the listener behind a higher-level “active input” flag so only the focused editor’s popup consumes navigation keys.
mpp-vscode/webview/src/components/Timeline.tsx (1)
63-77: Prefer explicit timeline item types instead of inline{ success: boolean; message: string }
TaskCompleteItemRendererandErrorItemRenderercurrently accept structural{ success: boolean; message: string }/{ message: string }, even thoughTimelineItemalready carries discriminated variants. Using the specific timeline item types here (e.g.TaskCompleteTimelineItem,ErrorTimelineItem) would tighten typing and keep these renderers aligned with any future schema changes.Also applies to: 133-145
mpp-vscode/src/services/ide-server.ts (1)
194-203: Port file is not cleaned up on stop.The port file written in
writePortFile()is never deleted when the server stops. This leaves stale connection info (including the auth token) on disk, which could cause confusion for external tools or represent a minor security concern.async stop(): Promise<void> { if (this.server) { return new Promise((resolve) => { this.server!.close(() => { this.log('IDE Server stopped'); + this.deletePortFile(); resolve(); }); }); } } + private async deletePortFile(): Promise<void> { + if (this.portFile) { + try { + await fs.unlink(this.portFile); + this.log(`Port file deleted: ${this.portFile}`); + } catch { + // Ignore if file doesn't exist + } + } + }mpp-core/src/commonTest/kotlin/cc/unitmesh/agent/tool/impl/PlanManagementToolTest.kt (1)
51-74: Consider adding edge case tests for invalid indices.The COMPLETE_STEP and FAIL_STEP tests use valid indices. Consider adding tests for out-of-bounds
taskIndexorstepIndexto ensure graceful error handling.@Test fun `should return error for invalid task index on COMPLETE_STEP`() = runTest { val tool = createTool() // Create a plan first val createParams = PlanManagementParams( action = "CREATE", planMarkdown = "1. Task\n - [ ] Step 1" ) tool.createInvocation(createParams).execute(ToolExecutionContext()) // Try to complete with invalid task index val params = PlanManagementParams(action = "COMPLETE_STEP", taskIndex = 99, stepIndex = 1) val result = tool.createInvocation(params).execute(ToolExecutionContext()) assertIs<ToolResult.Error>(result) }mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/AgentPlan.kt (2)
119-137:idCounteris not thread-safe in Kotlin Multiplatform context.The
idCounteruses a simple++idCounterwhich is not atomic. In concurrent scenarios (e.g., multiple coroutines creating plans), this could produce duplicate IDs. The same pattern exists inPlanTask.ktandPlanStep.ktper the provided snippets.For thread-safe ID generation, consider using
AtomicLong(on JVM) or a platform-specific solution:companion object { - private var idCounter = 0L + private val idCounter = kotlinx.atomicfu.atomic(0L) fun generateId(): String { - return "plan_${++idCounter}_${Clock.System.now().toEpochMilliseconds()}" + return "plan_${idCounter.incrementAndGet()}_${Clock.System.now().toEpochMilliseconds()}" } }Alternatively, rely solely on timestamps with random components (similar to
RendererModels.ktpattern) to avoid shared mutable state.
21-22: Mutable list in data class can cause serialization and copy issues.Using
MutableListas a property in a data class can lead to unexpected behavior withcopy()(shallow copy shares the list) and may complicate serialization. Consider using an immutableListwith builder methods that return new instances, or document the intended mutation pattern.If mutation is required, consider encapsulating it:
data class AgentPlan( val id: String, private val _tasks: MutableList<PlanTask> = mutableListOf(), // ... ) { val tasks: List<PlanTask> get() = _tasks fun addTask(task: PlanTask): AgentPlan { _tasks.add(task) touch() return this } }mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanTask.kt (3)
3-3: Unused import oftoMarker.
TaskStatus.Companion.toMarkeris imported but not used in this file. OnlyfromMarkeris used on line 146.-import cc.unitmesh.agent.plan.TaskStatus.Companion.toMarker
149-153: Same thread-safety concern asAgentPlan.generateId().The
idCounterpattern here has the same non-atomic increment issue. Consider applying a consistent fix acrossAgentPlan,PlanTask, andPlanStep(as noted in the external snippets).
83-86: Silent no-op when step ID is not found.
updateStepStatussilently ignores invalidstepIdvalues. This may be intentional, but consider returning a Boolean or throwing to help callers detect programming errors.- fun updateStepStatus(stepId: String, newStatus: TaskStatus) { - steps.find { it.id == stepId }?.updateStatus(newStatus) + fun updateStepStatus(stepId: String, newStatus: TaskStatus): Boolean { + val step = steps.find { it.id == stepId } ?: return false + step.updateStatus(newStatus) updateStatusFromSteps() + return true }mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanStep.kt (3)
29-29: Mutablevarin data class affectsequals/hashCodestability.Using
var statusin a data class means objects may produce different hash codes over their lifetime. IfPlanStepis used as a key in aMapor stored in aSet, status changes could break collection invariants.Consider either:
- Making
statusimmutable and returning new instances viacopy(status = newStatus)- Excluding
statusfrom data class generation and managing it separately
79-79: Emoji in regex may cause issues on WASM platform.The
STEP_PATTERNregex includes✓which, per coding guidelines, should be avoided for WASM platform compatibility. Consider using only ASCII alternatives likex,X, or other markers.- private val STEP_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX!*✓]?)\\s*]\\s*(.*)") + private val STEP_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX!*]?)\\s*]\\s*(.*)")As per coding guidelines, avoid emoji and UTF-8 in code for WASM platform.
113-122:idCounterincrement is not thread-safe; consider using atomic operations.The
++idCounteroperation is not atomic and can cause duplicate IDs in concurrent scenarios. Additionally, the ID generation pattern is inconsistent withRendererModels.ktwhich usesPlatform.getCurrentTimestamp()with a random component.Consider using
AtomicLongor aligning with the existing pattern inRendererModels.kt:- private var idCounter = 0L - - private fun generateId(): String { - return "step_${++idCounter}_${currentTimeMillis()}" - } + private val idCounter = kotlinx.atomicfu.atomic(0L) + + private fun generateId(): String { + return "step_${idCounter.incrementAndGet()}_${currentTimeMillis()}" + }mpp-vscode/src/services/status-bar.ts (1)
95-103: VSCode icon spin modifiers may conflict with manual animation.The
$(loading~spin),$(sync~spin), and$(gear~spin)icons use VSCode's built-in~spinmodifier which already provides animation. Cycling through these frames viasetIntervalmay create a jarring double-animation effect.Consider either:
- Using non-spinning icons and relying on your frame animation
- Using a single spinning icon without manual frame cycling
private getThinkingIcon(): string { - const frames = ['$(loading~spin)', '$(sync~spin)', '$(gear~spin)']; - return frames[this.animationFrame % frames.length]; + return '$(loading~spin)'; }mpp-vscode/syntaxes/DevIns.tmLanguage.json (1)
19-24: FrontMatter end pattern may not match correctly.The end pattern
(^|\\G)-{3}|\\.{3}\\s*$seems to have precedence issues. The|operator in regex has low precedence, so this matches either(^|\\G)-{3}or\\.{3}\\s*$, but the intent appears to be matching---or...at line start.Consider grouping:
- "end": "(^|\\G)-{3}|\\.{3}\\s*$" + "end": "(^|\\G)(-{3}|\\.{3})\\s*$"mpp-vscode/webview/src/components/TopToolbar.tsx (1)
30-41: MoveContextIndicatoroutside the component to prevent recreation on each render.Defining
ContextIndicatorinsideTopToolbarcauses a new function reference on every render, which can lead to unnecessary re-renders of child components.+const ContextIndicator: React.FC<{ + autoAddCurrentFile: boolean; + onToggleAutoAdd?: () => void; +}> = ({ autoAddCurrentFile, onToggleAutoAdd }) => ( + <button + className={`context-indicator ${autoAddCurrentFile ? 'active' : ''}`} + onClick={onToggleAutoAdd} + title={autoAddCurrentFile ? 'Auto-add current file: ON' : 'Auto-add current file: OFF'} + > + <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"> + <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> + </svg> + <span className="indicator-dot" /> + </button> +); + export const TopToolbar: React.FC<TopToolbarProps> = ({ selectedFiles, onAddFile, onRemoveFile, onClearFiles, autoAddCurrentFile = true, onToggleAutoAdd }) => { // ... - const ContextIndicator = () => ( /* ... */ );Then update usage to
<ContextIndicator autoAddCurrentFile={autoAddCurrentFile} onToggleAutoAdd={onToggleAutoAdd} />.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/PlanPanel.kt (2)
71-78: Unusual pattern:planIcon()explicitly returnsUnitwhile rendering content.The function signature
fun TaskStatus.planIcon(): Unitis misleading. It renders anIconcomposable but explicitly declaresUnitreturn type. Consider either:
- Remove the explicit return type (Kotlin infers
Unit)- Rename to
PlanIcon()following Compose naming conventions for component functions@Composable -fun TaskStatus.planIcon(): Unit = when (this) { +fun TaskStatus.PlanIcon() { + when (this) { TaskStatus.TODO -> Icon(Icons.Default.RadioButtonUnchecked, null, tint = planColor) // ... + } }
199-221: Animation resources created even when not used.The
infiniteTransitionandangleanimation are created for everyPlanStepItem, but only used whenstep.status == TaskStatus.IN_PROGRESS. This creates unnecessary animation overhead for most steps.Consider conditionally creating the animation:
@Composable private fun PlanStepItem(step: PlanStep, onClick: (() -> Unit)?) { - val infiniteTransition = rememberInfiniteTransition() - val angle by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable(animation = tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Restart) - ) + val angle = if (step.status == TaskStatus.IN_PROGRESS) { + val infiniteTransition = rememberInfiniteTransition() + infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable(animation = tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Restart) + ).value + } else 0fmpp-vscode/webview/src/components/ChatInput.tsx (1)
157-165: Keyboard handling incomplete when completion popup is open.When
completionOpenis true, the function returns early without preventing default. This means Enter key events will still bubble up. Consider letting theCompletionPopupcomponent handle its own keyboard events or explicitly handle arrow keys/Enter for completion navigation here.const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { - // Don't submit if completion popup is open - if (completionOpen) return; + // Let completion popup handle its keyboard events + if (completionOpen) { + // Prevent submit on Enter when popup is open + if (e.key === 'Enter') { + e.preventDefault(); + } + return; + } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); } };mpp-vscode/webview/src/components/FileChip.css (3)
20-22: Hardcoded fallback color violates coding guidelines.The fallback
#73c991is a hardcoded color. As per coding guidelines, colors should always use design tokens for consistency across platforms.Consider removing the fallback or using another CSS variable:
.file-chip.directory { - border-color: var(--vscode-gitDecoration-untrackedResourceForeground, #73c991); + border-color: var(--vscode-gitDecoration-untrackedResourceForeground, var(--foreground)); }Based on coding guidelines: "DO NOT hardcode colors... Always use design tokens."
32-34: Same hardcoded fallback color issue..file-chip.directory .file-chip-icon { - color: var(--vscode-gitDecoration-untrackedResourceForeground, #73c991); + color: var(--vscode-gitDecoration-untrackedResourceForeground, var(--foreground)); }
69-72: Hardcoded RGBA fallback color.The
rgba(255, 0, 0, 0.2)fallback is a hardcoded color..file-chip-remove:hover { opacity: 1 !important; - background: var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.2)); + background: var(--vscode-inputValidation-errorBackground); }If the fallback is essential, consider defining a CSS custom property at the root level.
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt (1)
28-39: Inconsistent truncation logic betweentruncatedPathandtruncatePath.The
truncatedPathproperty usestakeLast(4)with a 40-char threshold, while the standalonetruncatePathfunction usestakeLast(3)with a configurablemaxLength(default 30). The doc comment for both says "last 3-4 parts" which is ambiguous.Consider consolidating the logic to avoid duplication and ensure consistency:
val truncatedPath: String get() { - val parentPath = relativePath.substringBeforeLast("/", "") - if (parentPath.isEmpty()) return "" - if (parentPath.length <= 40) return parentPath - - val parts = parentPath.split("/") - if (parts.size <= 2) return "...$parentPath" - - val keepParts = parts.takeLast(4) - return "...${keepParts.joinToString("/")}" + return truncatePath(relativePath, maxLength = 40) }And update
truncatePathto handle the logic consistently (adjustingtakeLastcount as needed).Also applies to: 61-75
mpp-vscode/src/providers/chat-view.ts (2)
577-603: Using Node.jsfsmodule instead of VSCode API.The
openMcpConfigmethod uses Node.jsfs.promisesdirectly. For consistency and better cross-platform support in VSCode extensions, prefervscode.workspace.fs.private async openMcpConfig(): Promise<void> { try { const homeDir = process.env.HOME || process.env.USERPROFILE || ''; const mcpConfigPath = `${homeDir}/.autodev/mcp.json`; + const uri = vscode.Uri.file(mcpConfigPath); - const fs = await import('fs').then(m => m.promises); try { - await fs.access(mcpConfigPath); + await vscode.workspace.fs.stat(uri); } catch { const defaultConfig = { mcpServers: {} }; - await fs.mkdir(`${homeDir}/.autodev`, { recursive: true }); - await fs.writeFile(mcpConfigPath, JSON.stringify(defaultConfig, null, 2)); + const dirUri = vscode.Uri.file(`${homeDir}/.autodev`); + await vscode.workspace.fs.createDirectory(dirUri); + await vscode.workspace.fs.writeFile(uri, Buffer.from(JSON.stringify(defaultConfig, null, 2))); } - const uri = vscode.Uri.file(mcpConfigPath); await vscode.window.showTextDocument(uri);
972-979: Generated nonce is not used in CSP.The
getNonce()method generates a nonce, but it's called at line 884 and never actually used in the Content-Security-Policy header (line 892). The CSP uses'unsafe-inline'instead.Either remove the unused
getNonce()call or implement proper nonce-based CSP:- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource} 'unsafe-inline';"> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'nonce-${nonce}'; script-src ${webview.cspSource} 'nonce-${nonce}';">Then add
nonce="${nonce}"to inline script tags.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt (1)
47-84: Potential race condition inbuildIndex().If
buildIndex()is called concurrently from multiple coroutines, the check at lines 63-67 may pass for both before either setsINDEXING, causing duplicate indexing work. Consider using a mutex or atomic state transition.+import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + class WorkspaceFileSearchProvider( private val recentFilesProvider: RecentFilesProvider? = null ) : FileSearchProvider { + private val indexMutex = Mutex() // ... existing fields ... - suspend fun buildIndex() = withContext(Dispatchers.Default) { + suspend fun buildIndex() = indexMutex.withLock { + withContext(Dispatchers.Default) { // ... existing implementation ... + } }mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt (1)
95-113: Debounce implementation may produce out-of-order results.Rapid typing triggers multiple coroutines; the last to complete wins regardless of query order. Consider canceling previous search jobs or using a
MutableStateFlowwithdebounce().// Alternative: Track and cancel previous job private var searchJob: Job? = null fun performSearch(query: String) { if (query.length < 2 || currentWorkspace == null || indexingState != IndexingState.READY) { searchResults = emptyList() isLoading = false return } isLoading = true searchJob?.cancel() searchJob = scope.launch { delay(150) try { searchResults = searchProvider.searchFiles(query) } catch (e: Exception) { searchResults = emptyList() } finally { isLoading = false } } }mpp-vscode/test/mocks/vscode.ts (1)
14-17:Uri.parsemay throw on malformed URIs.The
new URL(value)constructor throws for invalid URLs. Consider adding try-catch or documenting this behavior for test authors.parse: (value: string) => { - const url = new URL(value); - return { scheme: url.protocol.replace(':', ''), path: url.pathname, fsPath: url.pathname, toString: () => value }; + try { + const url = new URL(value); + return { scheme: url.protocol.replace(':', ''), path: url.pathname, fsPath: url.pathname, toString: () => value }; + } catch { + // Fallback for non-URL strings (e.g., "file.txt") + return { scheme: 'file', path: value, fsPath: value, toString: () => value }; + } }mpp-vscode/src/services/diff-manager.ts (1)
250-259: Type assertion ontab.inputmay be fragile.The cast to
{ modified?: vscode.Uri; original?: vscode.Uri }assumes the diff input structure. Consider using optional chaining with more defensive checks.for (const tabGroup of vscode.window.tabGroups.all) { for (const tab of tabGroup.tabs) { - const input = tab.input as { modified?: vscode.Uri; original?: vscode.Uri }; - if (input && input.modified?.toString() === rightDocUri.toString()) { + const input = tab.input; + if (input && typeof input === 'object' && 'modified' in input) { + const diffInput = input as { modified?: vscode.Uri }; + if (diffInput.modified?.toString() === rightDocUri.toString()) { await vscode.window.tabGroups.close(tab); return; + } } } }mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/impl/PlanManagementTool.kt (1)
70-84: Redundant parsing inupdatePlan().Line 74 parses
planMarkdownintotasks, but if no plan exists (line 75),createPlanFromMarkdownis called which parses the markdown again internally. Consider reusing the parsed tasks.private fun updatePlan(): ToolResult { if (params.planMarkdown.isBlank()) { return ToolResult.Error("planMarkdown is required for UPDATE", ToolErrorType.MISSING_REQUIRED_PARAMETER.code) } val tasks = MarkdownPlanParser.parse(params.planMarkdown) if (planStateService.currentPlan.value == null) { - val plan = planStateService.createPlanFromMarkdown(params.planMarkdown) + val plan = planStateService.createPlan(tasks) return ToolResult.Success("Plan created with ${plan.taskCount} tasks.\n\n${plan.toMarkdown()}", mapOf("plan_id" to plan.id, "task_count" to plan.taskCount.toString())) } // ... rest unchanged }
| private suspend fun executePlanManagementTool( | ||
| tool: Tool, | ||
| params: Map<String, Any>, | ||
| context: cc.unitmesh.agent.tool.ToolExecutionContext | ||
| ): ToolResult { | ||
| val taskBoundaryTool = tool as cc.unitmesh.agent.tool.impl.TaskBoundaryTool | ||
|
|
||
| val taskName = params["taskName"] as? String | ||
| ?: return ToolResult.Error("taskName parameter is required") | ||
| val status = params["status"] as? String | ||
| ?: return ToolResult.Error("status parameter is required") | ||
| val summary = params["summary"] as? String ?: "" | ||
|
|
||
| val taskBoundaryParams = cc.unitmesh.agent.tool.impl.TaskBoundaryParams( | ||
| taskName = taskName, | ||
| status = status, | ||
| summary = summary | ||
| val planTool = tool as cc.unitmesh.agent.tool.impl.PlanManagementTool | ||
|
|
||
| val action = params["action"] as? String | ||
| ?: return ToolResult.Error("action parameter is required") | ||
| val planMarkdown = params["planMarkdown"] as? String ?: "" | ||
|
|
||
| // Handle taskIndex and stepIndex - can be Number or String | ||
| val taskIndex = when (val v = params["taskIndex"]) { | ||
| is Number -> v.toInt() | ||
| is String -> v.toIntOrNull() ?: 0 | ||
| else -> 0 | ||
| } | ||
| val stepIndex = when (val v = params["stepIndex"]) { | ||
| is Number -> v.toInt() | ||
| is String -> v.toIntOrNull() ?: 0 | ||
| else -> 0 | ||
| } | ||
|
|
||
| val planParams = cc.unitmesh.agent.tool.impl.PlanManagementParams( | ||
| action = action, | ||
| planMarkdown = planMarkdown, | ||
| taskIndex = taskIndex, | ||
| stepIndex = stepIndex | ||
| ) | ||
| val invocation = taskBoundaryTool.createInvocation(taskBoundaryParams) | ||
|
|
||
| val invocation = planTool.createInvocation(planParams) | ||
| return invocation.execute(context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stricter validation for taskIndex/stepIndex would avoid mis-targeting steps
In executePlanManagementTool, both indices are derived as:
val taskIndex = when (val v = params["taskIndex"]) {
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
}This means a bad value like "abc" silently becomes 0, and missing indices are indistinguishable from an explicit 0. For plan operations this could accidentally act on the first task/step instead of failing fast.
Consider:
- Treating parse failures as an error (
ToolResult.Error("taskIndex must be an integer")), and - Using a sentinel (e.g.
-1) or nullable field inPlanManagementParamsto represent “no index provided”, if the tool supports actions that don’t require indices.
This will make plan actions safer and easier to debug when the LLM or caller sends bad parameters.
Also applies to: 383-392
🤖 Prompt for AI Agents
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt
around lines 678-709 (and also adjust same pattern at 383-392): the current
parsing of taskIndex/stepIndex silently maps invalid or missing values to 0
which can accidentally target the first task/step; instead, validate inputs and
fail fast on parse errors and distinguish “not provided” from an explicit 0 — if
params contain a non-null value that is a String attempt to parse it and return
ToolResult.Error("taskIndex must be an integer") or ToolResult.Error("stepIndex
must be an integer") on parse failure, and for absent keys or nulls pass a
sentinel (e.g. -1) or use nullable Ints in PlanManagementParams (update the
PlanManagementParams type accordingly), then construct the invocation with the
corrected/tightened taskIndex/stepIndex values so bad input is rejected rather
than defaulting to 0.
| */ | ||
| val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow() | ||
|
|
||
| private val listeners = mutableListOf<PlanUpdateListener>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread-safety concern with listener collection.
The mutableListOf is not thread-safe. If addListener/removeListener are called from different coroutines while notifyX methods iterate, a ConcurrentModificationException can occur.
Consider using a thread-safe collection:
- private val listeners = mutableListOf<PlanUpdateListener>()
+ private val listeners = mutableListOf<PlanUpdateListener>()
+ private val listenersLock = Any()Then synchronize access in addListener, removeListener, and notification methods, or use CopyOnWriteArrayList if available on all target platforms.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanStateService.kt
around line 26, the listeners field is initialized with mutableListOf which is
not thread-safe; replace it with a thread-safe collection (e.g.,
java.util.concurrent.CopyOnWriteArrayList when available on targets) or wrap
access with a mutex/synchronized block, and update addListener, removeListener
and all notification methods to use the chosen synchronization strategy (either
operate directly on the CopyOnWriteArrayList or acquire the mutex before
mutating/iterating) to prevent ConcurrentModificationException from concurrent
coroutine access.
| fun updatePlan(tasks: List<PlanTask>) { | ||
| val plan = _currentPlan.value | ||
| if (plan != null) { | ||
| plan.tasks.clear() | ||
| plan.tasks.addAll(tasks) | ||
| notifyPlanUpdated(plan) | ||
| } else { | ||
| createPlan(tasks) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StateFlow may not emit updates when mutating in-place.
StateFlow uses structural equality to determine if a new value should be emitted. Mutating plan.tasks in-place (via clear() and addAll()) without reassigning _currentPlan.value means collectors won't receive update notifications since the object reference hasn't changed.
Consider creating a new AgentPlan instance or explicitly re-emitting:
fun updatePlan(tasks: List<PlanTask>) {
val plan = _currentPlan.value
if (plan != null) {
plan.tasks.clear()
plan.tasks.addAll(tasks)
+ _currentPlan.value = plan // Force re-emission
notifyPlanUpdated(plan)
} else {
createPlan(tasks)
}
}Note: Even with re-assignment, StateFlow checks equality. You may need to create a copy or use a different state holder pattern for mutable data.
🤖 Prompt for AI Agents
In mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanStateService.kt
around lines 62 to 71, the code mutates the existing AgentPlan.tasks in-place so
StateFlow may not emit updates because the value reference doesn't change;
instead create a new AgentPlan instance (or a copy with the updated tasks) and
assign it to _currentPlan.value so the StateFlow sees a new value, and then call
notifyPlanUpdated with that new instance (or otherwise re-emit the updated
plan).
| fun addTask(task: PlanTask) { | ||
| val plan = _currentPlan.value ?: createPlan(emptyList()) | ||
| plan.addTask(task) | ||
| notifyPlanUpdated(plan) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same StateFlow mutation issue in addTask.
Similar to updatePlan, calling plan.addTask(task) mutates the existing plan without re-emitting the StateFlow value.
Apply the same fix pattern as suggested for updatePlan.
🤖 Prompt for AI Agents
In mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanStateService.kt
around lines 84 to 88, addTask currently mutates the existing Plan instance
(plan.addTask(task)) which won't re-emit the StateFlow; instead create an
updated Plan instance (e.g., copy the plan with a new tasks list that includes
the new task or use an immutable builder), then assign that new Plan to the
StateFlow (or call notifyPlanUpdated with the new Plan) so the StateFlow value
is replaced rather than mutated in-place.
| println("[DefaultFileSystem] searchFiles called: pattern=$pattern, projectPath=$projectPath") | ||
| val projectRoot = Path.of(projectPath) | ||
| if (!projectRoot.exists() || !projectRoot.isDirectory()) { | ||
| println("[DefaultFileSystem] Project root does not exist or is not a directory") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace println with a proper logging framework.
Using println for logging is not production-ready and should be replaced with a proper logging framework (e.g., SLF4J, Kotlin logging) to enable:
- Log level control (debug/info/warn/error)
- Conditional enabling/disabling
- Proper output redirection
- Structured logging
Consider using a logging framework like this:
private val logger = LoggerFactory.getLogger(DefaultFileSystem::class.java)
actual override fun searchFiles(pattern: String, maxDepth: Int, maxResults: Int): List<String> {
return try {
logger.debug("searchFiles called: pattern={}, projectPath={}", pattern, projectPath)
val projectRoot = Path.of(projectPath)
if (!projectRoot.exists() || !projectRoot.isDirectory()) {
logger.warn("Project root does not exist or is not a directory")
return emptyList()
}🤖 Prompt for AI Agents
In
mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt
around lines 108 to 111, replace the println calls with a proper logger: add a
private SLF4J LoggerFactory logger for DefaultFileSystem, use logger.debug for
the "searchFiles called" message with parameterized placeholders, and use
logger.warn for the "Project root does not exist or is not a directory" message;
ensure imports are added and no println() calls remain so logging can be
controlled by log levels and configuration.
| // Check if we need to show config prompt | ||
| const needsConfig = agentState.timeline.length === 0 && | ||
| agentState.currentStreamingContent.includes('No configuration found') || | ||
| agentState.currentStreamingContent.includes('Configuration Required'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Operator precedence issue in needsConfig logic.
The && has higher precedence than ||, causing incorrect evaluation. The current logic is: (A && B.includes('X')) || B.includes('Y') instead of the intended A && (B.includes('X') || B.includes('Y')).
// Check if we need to show config prompt
- const needsConfig = agentState.timeline.length === 0 &&
- agentState.currentStreamingContent.includes('No configuration found') ||
- agentState.currentStreamingContent.includes('Configuration Required');
+ const needsConfig = agentState.timeline.length === 0 && (
+ agentState.currentStreamingContent.includes('No configuration found') ||
+ agentState.currentStreamingContent.includes('Configuration Required')
+ );📝 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.
| // Check if we need to show config prompt | |
| const needsConfig = agentState.timeline.length === 0 && | |
| agentState.currentStreamingContent.includes('No configuration found') || | |
| agentState.currentStreamingContent.includes('Configuration Required'); | |
| // Check if we need to show config prompt | |
| const needsConfig = agentState.timeline.length === 0 && ( | |
| agentState.currentStreamingContent.includes('No configuration found') || | |
| agentState.currentStreamingContent.includes('Configuration Required') | |
| ); |
🤖 Prompt for AI Agents
In mpp-vscode/webview/src/App.tsx around lines 362 to 365, the boolean
expression for needsConfig mixes && and || without grouping so it evaluates as
(A && B.includes('No configuration found')) || B.includes('Configuration
Required') instead of the intended A && (B.includes('No configuration found') ||
B.includes('Configuration Required')). Fix by adding parentheses around the two
includes checks: ensure the timeline-empty check is ANDed with the combined OR
of the two includes calls, so the overall condition is timeline.length === 0 &&
(currentStreamingContent.includes(...) ||
currentStreamingContent.includes(...)).
| return ( | ||
| <div className="file-search-popup-overlay" onClick={onClose}> | ||
| <div className="file-search-popup" onClick={e => e.stopPropagation()}> | ||
| <div className="file-search-header"> | ||
| <input ref={inputRef} type="text" className="file-search-input" placeholder="Search files..." | ||
| value={searchQuery} onChange={e => setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} /> | ||
| </div> | ||
| <div className="file-search-list" ref={listRef}> | ||
| {isLoading && <div className="file-search-loading">Searching...</div>} | ||
| {!isLoading && filteredItems.length === 0 && ( | ||
| <div className="file-search-empty">{searchQuery.length >= 2 ? 'No files found' : 'Type to search files'}</div> | ||
| )} | ||
| {!isLoading && searchQuery.length < 2 && recentFiles.length > 0 && ( | ||
| <div className="file-search-section-title">Recent Files</div> | ||
| )} | ||
| {filteredItems.map((item, index) => ( | ||
| <FileSearchItem key={item.path} item={item} isSelected={index === selectedIndex} | ||
| onClick={() => { onSelectFile(item); setSearchQuery(''); onClose(); }} | ||
| onMouseEnter={() => setSelectedIndex(index)} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add ARIA attributes for accessibility.
The popup lacks accessibility markup. Screen reader users won't understand the component's purpose or be able to navigate it effectively.
Key additions needed:
- Dialog:
role="dialog"andaria-modal="true" - Input:
aria-label="Search files"or visible label - List:
role="listbox"witharia-label - Items:
role="option"andaria-selected
Example for the popup container:
- <div className="file-search-popup-overlay" onClick={onClose}>
- <div className="file-search-popup" onClick={e => e.stopPropagation()}>
+ <div className="file-search-popup-overlay" onClick={onClose} role="presentation">
+ <div className="file-search-popup" onClick={e => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Search files">📝 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.
| return ( | |
| <div className="file-search-popup-overlay" onClick={onClose}> | |
| <div className="file-search-popup" onClick={e => e.stopPropagation()}> | |
| <div className="file-search-header"> | |
| <input ref={inputRef} type="text" className="file-search-input" placeholder="Search files..." | |
| value={searchQuery} onChange={e => setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} /> | |
| </div> | |
| <div className="file-search-list" ref={listRef}> | |
| {isLoading && <div className="file-search-loading">Searching...</div>} | |
| {!isLoading && filteredItems.length === 0 && ( | |
| <div className="file-search-empty">{searchQuery.length >= 2 ? 'No files found' : 'Type to search files'}</div> | |
| )} | |
| {!isLoading && searchQuery.length < 2 && recentFiles.length > 0 && ( | |
| <div className="file-search-section-title">Recent Files</div> | |
| )} | |
| {filteredItems.map((item, index) => ( | |
| <FileSearchItem key={item.path} item={item} isSelected={index === selectedIndex} | |
| onClick={() => { onSelectFile(item); setSearchQuery(''); onClose(); }} | |
| onMouseEnter={() => setSelectedIndex(index)} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| return ( | |
| <div className="file-search-popup-overlay" onClick={onClose} role="presentation"> | |
| <div className="file-search-popup" onClick={e => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Search files"> | |
| <div className="file-search-header"> | |
| <input ref={inputRef} type="text" className="file-search-input" placeholder="Search files..." | |
| value={searchQuery} onChange={e => setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} /> | |
| </div> | |
| <div className="file-search-list" ref={listRef}> | |
| {isLoading && <div className="file-search-loading">Searching...</div>} | |
| {!isLoading && filteredItems.length === 0 && ( | |
| <div className="file-search-empty">{searchQuery.length >= 2 ? 'No files found' : 'Type to search files'}</div> | |
| )} | |
| {!isLoading && searchQuery.length < 2 && recentFiles.length > 0 && ( | |
| <div className="file-search-section-title">Recent Files</div> | |
| )} | |
| {filteredItems.map((item, index) => ( | |
| <FileSearchItem key={item.path} item={item} isSelected={index === selectedIndex} | |
| onClick={() => { onSelectFile(item); setSearchQuery(''); onClose(); }} | |
| onMouseEnter={() => setSelectedIndex(index)} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); |
🤖 Prompt for AI Agents
In mpp-vscode/webview/src/components/FileSearchPopup.tsx around lines 130 to
153, the popup is missing ARIA markup which makes it inaccessible; update the
outer popup container to include role="dialog" and aria-modal="true", add
aria-label (or visible label) to the input (e.g. aria-label="Search files"),
mark the list container with role="listbox" and an appropriate aria-label, and
ensure each FileSearchItem is rendered with role="option" and
aria-selected={isSelected} (and an id so you can use aria-activedescendant on
the listbox if you want keyboard focus tracking); keep click/keyboard behavior
the same but wire the selectedIndex boolean to the aria-selected attribute so
screen readers know the current selection.
| case 'mermaid': | ||
| case 'mmd': | ||
| // TODO: Implement MermaidRenderer | ||
| return ( | ||
| <CodeBlockRenderer | ||
| code={block.text} | ||
| language="mermaid" | ||
| isComplete={isComplete} | ||
| /> | ||
| ); | ||
|
|
||
| case 'devin': | ||
| // TODO: Implement DevInRenderer | ||
| return ( | ||
| <CodeBlockRenderer | ||
| code={block.text} | ||
| language="devin" | ||
| isComplete={isComplete} | ||
| /> | ||
| ); | ||
|
|
||
| case 'plan': | ||
| // TODO: Implement PlanRenderer | ||
| return ( | ||
| <CodeBlockRenderer | ||
| code={block.text} | ||
| language="plan" | ||
| isComplete={isComplete} | ||
| /> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing onAction prop on placeholder renderers.
The mermaid, devin, and plan cases use CodeBlockRenderer but don't pass the onAction callback. This means actions like "Insert" or "Apply" won't work for these block types.
case 'mermaid':
case 'mmd':
// TODO: Implement MermaidRenderer
return (
<CodeBlockRenderer
code={block.text}
language="mermaid"
isComplete={isComplete}
+ onAction={onAction}
/>
);
case 'devin':
// TODO: Implement DevInRenderer
return (
<CodeBlockRenderer
code={block.text}
language="devin"
isComplete={isComplete}
+ onAction={onAction}
/>
);
case 'plan':
// TODO: Implement PlanRenderer
return (
<CodeBlockRenderer
code={block.text}
language="plan"
isComplete={isComplete}
+ onAction={onAction}
/>
);🤖 Prompt for AI Agents
In mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx around lines 108
to 137, the mermaid, devin, and plan cases render CodeBlockRenderer without the
onAction prop so user actions (Insert/Apply) won't be handled; update each of
those returns to pass the same onAction handler used elsewhere in this component
(e.g., onAction={onAction} or the correct handler name in scope), keeping
existing props (code, language, isComplete) intact so the CodeBlockRenderer
receives onAction for interactive behavior.
| .timeline-item.error { | ||
| display: flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| gap: 8px; | ||
| padding: 12px; | ||
| border-radius: 6px; | ||
| background-color: rgba(244, 67, 54, 0.1); | ||
| border: 1px solid var(--vscode-testing-iconFailed, #f44336); | ||
| font-size: 13px; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same hardcoded color issue in error items.
Line 141 also uses a hardcoded rgba() value. Apply the same fix as suggested for task-complete items.
.timeline-item.error {
...
- background-color: rgba(244, 67, 54, 0.1);
+ background-color: color-mix(in srgb, var(--vscode-testing-iconFailed, #f44336) 10%, transparent);
border: 1px solid var(--vscode-testing-iconFailed, #f44336);
...
}🤖 Prompt for AI Agents
In mpp-vscode/webview/src/components/Timeline.css around lines 134 to 144, the
.timeline-item.error rule uses a hardcoded background-color rgba(244, 67, 54,
0.1); — replace that hardcoded rgba with the theme variable approach used for
task-complete items: derive the error color from --vscode-testing-iconFailed
(with a fallback like #f44336) and apply a 10% alpha (for example by introducing
a small helper css var for the RGB value or using color-mix with the var),
leaving the border as-is so the color source is consistent with the rest of the
UI.
| export interface TaskInfo { | ||
| taskName: string; | ||
| status: 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED'; | ||
| summary: string; | ||
| timestamp?: number; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TaskInfo status is missing 'PLANNING' value.
The TypeScript TaskInfo.status type excludes 'PLANNING' which exists in the Kotlin TaskStatus enum (in RendererModels.kt). This mismatch could cause runtime issues when the backend sends a task with PLANNING status.
export interface TaskInfo {
taskName: string;
- status: 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED';
+ status: 'PLANNING' | 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED';
summary: string;
timestamp?: number;
}📝 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.
| export interface TaskInfo { | |
| taskName: string; | |
| status: 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED'; | |
| summary: string; | |
| timestamp?: number; | |
| } | |
| export interface TaskInfo { | |
| taskName: string; | |
| status: 'PLANNING' | 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED'; | |
| summary: string; | |
| timestamp?: number; | |
| } |
🤖 Prompt for AI Agents
In mpp-vscode/webview/src/types/timeline.ts around lines 34 to 39, the
TaskInfo.status union type omits the 'PLANNING' value present in the backend
Kotlin TaskStatus enum; update the status type to include 'PLANNING' so it
becomes 'WORKING' | 'COMPLETED' | 'CANCELLED' | 'BLOCKED' | 'PLANNING' to match
the backend and avoid runtime mismatches.
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.