Skip to content

Commit 2a5de66

Browse files
committed
feat(vscode): implement plan functionality for mpp-vscode
- Add observePlanState method to JsCodingAgent for observing PlanStateService state changes - Extend JsPlanSummaryData with JsTaskSummary and JsStepSummary for complete task/step info - Add plan state observer in ChatViewProvider to sync plan updates to webview - Add planUpdate/planCleared message handling in App.tsx - Pass currentPlan prop to ChatInput component for PlanSummaryBar display - Add ws as external dependency in esbuild config
1 parent 60017ee commit 2a5de66

File tree

6 files changed

+179
-3
lines changed

6 files changed

+179
-3
lines changed

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/CodingAgentExports.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cc.unitmesh.agent.config.JsToolConfigFile
44
import cc.unitmesh.agent.render.DefaultCodingAgentRenderer
55
import cc.unitmesh.llm.JsMessage
66
import kotlinx.coroutines.GlobalScope
7+
import kotlinx.coroutines.launch
78
import kotlinx.coroutines.promise
89
import kotlin.js.Promise
910

@@ -182,6 +183,34 @@ class JsCodingAgent(
182183
JsMessage(msg.role.name.lowercase(), msg.content)
183184
}.toTypedArray()
184185
}
186+
187+
/**
188+
* Observe plan state changes and call the callback with plan summary data.
189+
* Returns a function to stop observing.
190+
*/
191+
@JsName("observePlanState")
192+
fun observePlanState(callback: (JsPlanSummaryData?) -> Unit): () -> Unit {
193+
val planStateService = agent.getPlanStateService()
194+
if (planStateService == null) {
195+
return { }
196+
}
197+
198+
var job: kotlinx.coroutines.Job? = null
199+
job = GlobalScope.launch {
200+
planStateService.currentPlan.collect { plan ->
201+
if (plan != null) {
202+
val summary = cc.unitmesh.agent.plan.PlanSummaryData.from(plan)
203+
callback(JsPlanSummaryData.from(summary))
204+
} else {
205+
callback(null)
206+
}
207+
}
208+
}
209+
210+
return {
211+
job.cancel()
212+
}
213+
}
185214
}
186215

187216

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
package cc.unitmesh.agent
22

33
import cc.unitmesh.agent.plan.PlanSummaryData
4+
import cc.unitmesh.agent.plan.StepSummary
5+
import cc.unitmesh.agent.plan.TaskSummary
46
import cc.unitmesh.agent.render.CodingAgentRenderer
57
import kotlin.js.JsExport
68

9+
/**
10+
* JS-friendly step summary data
11+
*/
12+
@JsExport
13+
data class JsStepSummary(
14+
val id: String,
15+
val description: String,
16+
val status: String
17+
) {
18+
companion object {
19+
fun from(step: StepSummary): JsStepSummary {
20+
return JsStepSummary(
21+
id = step.id,
22+
description = step.description,
23+
status = step.status.name
24+
)
25+
}
26+
}
27+
}
28+
29+
/**
30+
* JS-friendly task summary data
31+
*/
32+
@JsExport
33+
data class JsTaskSummary(
34+
val id: String,
35+
val title: String,
36+
val status: String,
37+
val completedSteps: Int,
38+
val totalSteps: Int,
39+
val steps: Array<JsStepSummary>
40+
) {
41+
companion object {
42+
fun from(task: TaskSummary): JsTaskSummary {
43+
return JsTaskSummary(
44+
id = task.id,
45+
title = task.title,
46+
status = task.status.name,
47+
completedSteps = task.completedSteps,
48+
totalSteps = task.totalSteps,
49+
steps = task.steps.map { JsStepSummary.from(it) }.toTypedArray()
50+
)
51+
}
52+
}
53+
}
54+
755
/**
856
* JS-friendly plan summary data
957
*/
@@ -16,7 +64,8 @@ data class JsPlanSummaryData(
1664
val failedSteps: Int,
1765
val progressPercent: Int,
1866
val status: String,
19-
val currentStepDescription: String?
67+
val currentStepDescription: String?,
68+
val tasks: Array<JsTaskSummary>
2069
) {
2170
companion object {
2271
fun from(summary: PlanSummaryData): JsPlanSummaryData {
@@ -28,7 +77,8 @@ data class JsPlanSummaryData(
2877
failedSteps = summary.failedSteps,
2978
progressPercent = summary.progressPercent,
3079
status = summary.status.name,
31-
currentStepDescription = summary.currentStepDescription
80+
currentStepDescription = summary.currentStepDescription,
81+
tasks = summary.tasks.map { JsTaskSummary.from(it) }.toTypedArray()
3282
)
3383
}
3484
}

mpp-vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
"scripts": {
139139
"vscode:prepublish": "npm run build",
140140
"build": "npm run build:extension && npm run build:webview",
141-
"build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
141+
"build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --external:ws --format=cjs --platform=node",
142142
"build:webview": "cd webview && npm install && npm run build",
143143
"watch": "npm run build:extension -- --watch",
144144
"package": "vsce package",

mpp-vscode/src/providers/chat-view.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
3030
private isExecuting = false;
3131
private messages: Array<{ role: string; content: string }> = [];
3232
private editorChangeDisposable: vscode.Disposable | undefined;
33+
private planStateUnsubscribe: (() => void) | null = null;
3334

3435
constructor(
3536
private readonly context: vscode.ExtensionContext,
@@ -329,9 +330,46 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
329330
);
330331

331332
this.log(`CodingAgent initialized for workspace: ${workspacePath}`);
333+
334+
// Start observing plan state changes
335+
this.startPlanStateObserver();
336+
332337
return this.codingAgent;
333338
}
334339

340+
/**
341+
* Start observing plan state changes from CodingAgent
342+
* Mirrors IdeaAgentViewModel.startPlanStateObserver()
343+
*/
344+
private startPlanStateObserver(): void {
345+
// Cancel any existing observer
346+
if (this.planStateUnsubscribe) {
347+
this.planStateUnsubscribe();
348+
this.planStateUnsubscribe = null;
349+
}
350+
351+
if (!this.codingAgent) {
352+
return;
353+
}
354+
355+
try {
356+
// Use the observePlanState method from JsCodingAgent
357+
this.planStateUnsubscribe = this.codingAgent.observePlanState((planSummary: any) => {
358+
if (planSummary) {
359+
this.postMessage({
360+
type: 'planUpdate',
361+
data: this.convertPlanSummary(planSummary)
362+
});
363+
} else {
364+
this.postMessage({ type: 'planCleared' });
365+
}
366+
});
367+
this.log('Plan state observer started');
368+
} catch (error) {
369+
this.log(`Failed to start plan state observer: ${error}`);
370+
}
371+
}
372+
335373
/**
336374
* Create renderer that forwards events to webview
337375
* Mirrors TuiRenderer from mpp-ui
@@ -392,10 +430,47 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
392430
addLiveTerminal: () => {},
393431
forceStop: () => {
394432
self.postMessage({ type: 'taskComplete', data: { success: false, message: 'Stopped' } });
433+
},
434+
// Plan summary rendering - sends plan data to webview
435+
renderPlanSummary: (summary: any) => {
436+
self.postMessage({
437+
type: 'planUpdate',
438+
data: self.convertPlanSummary(summary)
439+
});
395440
}
396441
};
397442
}
398443

444+
/**
445+
* Convert JsPlanSummaryData to webview-compatible format
446+
*/
447+
private convertPlanSummary(summary: any): any {
448+
if (!summary) return null;
449+
450+
return {
451+
planId: summary.planId,
452+
title: summary.title,
453+
totalSteps: summary.totalSteps,
454+
completedSteps: summary.completedSteps,
455+
failedSteps: summary.failedSteps,
456+
progressPercent: summary.progressPercent,
457+
status: summary.status,
458+
currentStepDescription: summary.currentStepDescription,
459+
tasks: Array.from(summary.tasks || []).map((task: any) => ({
460+
id: task.id,
461+
title: task.title,
462+
status: task.status,
463+
completedSteps: task.completedSteps,
464+
totalSteps: task.totalSteps,
465+
steps: Array.from(task.steps || []).map((step: any) => ({
466+
id: step.id,
467+
description: step.description,
468+
status: step.status
469+
}))
470+
}))
471+
};
472+
}
473+
399474
/**
400475
* Handle user message
401476
* Mirrors IdeaAgentViewModel.executeTask()

mpp-vscode/webview/src/App.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ChatInput } from './components/ChatInput';
1111
import { ModelConfig } from './components/ModelSelector';
1212
import { SelectedFile } from './components/FileChip';
1313
import { CompletionItem } from './components/CompletionPopup';
14+
import { PlanData } from './components/plan';
1415
import { useVSCode, ExtensionMessage } from './hooks/useVSCode';
1516
import type { AgentState, ToolCallInfo, TerminalOutput, ToolCallTimelineItem } from './types/timeline';
1617
import './App.css';
@@ -53,6 +54,9 @@ const App: React.FC = () => {
5354
const [completionItems, setCompletionItems] = useState<CompletionItem[]>([]);
5455
const [completionResult, setCompletionResult] = useState<CompletionResult | null>(null);
5556

57+
// Plan state - mirrors mpp-idea's IdeaPlanSummaryBar
58+
const [currentPlan, setCurrentPlan] = useState<PlanData | null>(null);
59+
5660
const { postMessage, onMessage, isVSCode } = useVSCode();
5761

5862
// Handle messages from extension
@@ -253,6 +257,20 @@ const App: React.FC = () => {
253257
});
254258
}
255259
break;
260+
261+
// Plan update from mpp-core PlanStateService
262+
case 'planUpdate':
263+
if (msg.data) {
264+
setCurrentPlan(msg.data as unknown as PlanData);
265+
} else {
266+
setCurrentPlan(null);
267+
}
268+
break;
269+
270+
// Plan cleared
271+
case 'planCleared':
272+
setCurrentPlan(null);
273+
break;
256274
}
257275
}, []);
258276

@@ -434,6 +452,7 @@ const App: React.FC = () => {
434452
currentConfigName={configState.currentConfigName}
435453
totalTokens={totalTokens}
436454
activeFile={activeFile}
455+
currentPlan={currentPlan}
437456
/>
438457
</div>
439458
);

mpp-vscode/webview/src/hooks/useVSCode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export interface ExtensionMessage {
4848
// Completion events
4949
| 'completionsResult'
5050
| 'completionApplied'
51+
// Plan events
52+
| 'planUpdate'
53+
| 'planCleared'
5154
// Error and control
5255
| 'error'
5356
| 'historyCleared';

0 commit comments

Comments
 (0)