@@ -124,6 +124,17 @@ function toolResultObj(
124124/** Cursor's file-edit tool surfaces with this name (its `toolCall.type`). */
125125const EDIT_TOOL_NAME = "edit" ;
126126
127+ /** Cursor plan-mode tool; markdown lives in call args, result is empty. */
128+ const CREATE_PLAN_TOOL_NAME = "createPlan" ;
129+
130+ function isCreatePlanTool ( name : string ) : boolean {
131+ return name === CREATE_PLAN_TOOL_NAME ;
132+ }
133+
134+ function createPlanContent ( input : unknown ) : string | undefined {
135+ return strField ( input , "plan" ) ;
136+ }
137+
127138function isRecord ( v : unknown ) : v is Record < string , unknown > {
128139 return typeof v === "object" && v !== null ;
129140}
@@ -285,9 +296,10 @@ function mapTodos(args: unknown): Array<{ content: string; status: string }> {
285296/**
286297 * Cursor tool name → opencode native tool adapter. Cursor tools without a
287298 * natural opencode counterpart (`delete`, `mcp`, `semSearch`, `readLints`,
288- * `generateImage`, `createPlan`, `recordScreen`, `task`) are intentionally
289- * absent and fall through to generic `cursor_*` blocks. `edit` is handled
290- * separately (its native call input depends on the diff in the result).
299+ * `generateImage`, `recordScreen`) are intentionally absent and fall through
300+ * to generic `cursor_*` blocks. `edit` and `createPlan` are handled separately
301+ * (edit: native call input depends on the diff in the result; createPlan:
302+ * emitted as assistant markdown text so opencode renders it like a normal plan).
291303 */
292304const NATIVE_ADAPTERS : Record < string , NativeToolAdapter > = {
293305 // Cursor `shell` → opencode `bash` (console renderer).
@@ -743,10 +755,11 @@ interface OpenToolCall {
743755interface BlockToolState {
744756 open : Map < string , OpenToolCall > ;
745757 pendingEdits : Map < string , string > ;
758+ dropped : Set < string > ;
746759}
747760
748761function newBlockToolState ( ) : BlockToolState {
749- return { open : new Map ( ) , pendingEdits : new Map ( ) } ;
762+ return { open : new Map ( ) , pendingEdits : new Map ( ) , dropped : new Set ( ) } ;
750763}
751764
752765/** Parts to emit for a blocks-mode `tool-call` event (edits are buffered). */
@@ -991,6 +1004,20 @@ export function cursorEventsToStream(
9911004 reasoningLine ( event . text ) ;
9921005 break ;
9931006 case "tool-call" :
1007+ if ( isCreatePlanTool ( event . name ) ) {
1008+ const plan = createPlanContent ( event . input ) ;
1009+ if ( plan ) {
1010+ closeReasoning ( ) ;
1011+ streamedText = true ;
1012+ controller . enqueue ( {
1013+ type : "text-delta" ,
1014+ id : ensureText ( ) ,
1015+ delta : plan ,
1016+ } ) ;
1017+ }
1018+ toolState . dropped . add ( event . id ) ;
1019+ break ;
1020+ }
9941021 if ( toolDisplay === "blocks" ) {
9951022 const parts = blockToolCallParts (
9961023 event . id ,
@@ -1015,6 +1042,7 @@ export function cursorEventsToStream(
10151042 }
10161043 break ;
10171044 case "tool-result" :
1045+ if ( toolState . dropped . delete ( event . id ) ) break ;
10181046 if ( toolDisplay === "blocks" ) {
10191047 const parts = blockToolResultParts (
10201048 event . id ,
@@ -1109,6 +1137,12 @@ export async function cursorEventsToContent(
11091137 reasoning += event . text ;
11101138 break ;
11111139 case "tool-call" :
1140+ if ( isCreatePlanTool ( event . name ) ) {
1141+ const plan = createPlanContent ( event . input ) ;
1142+ if ( plan ) text += plan ;
1143+ toolState . dropped . add ( event . id ) ;
1144+ break ;
1145+ }
11121146 if ( toolDisplay === "blocks" ) {
11131147 for ( const part of blockToolCallParts (
11141148 event . id ,
@@ -1123,6 +1157,7 @@ export async function cursorEventsToContent(
11231157 }
11241158 break ;
11251159 case "tool-result" :
1160+ if ( toolState . dropped . delete ( event . id ) ) break ;
11261161 if ( toolDisplay === "blocks" ) {
11271162 for ( const part of blockToolResultParts (
11281163 event . id ,
0 commit comments