@@ -22,11 +22,14 @@ import {
2222export interface RunMistralOptions {
2323 prompt : string ;
2424 workingDir : string ;
25+ resumeSessionId ?: string ;
26+ resumePrompt ?: string ;
2527 model ?: string ;
2628 env ?: NodeJS . ProcessEnv ;
2729 onData ?: ( chunk : string ) => void ;
2830 onErrorData ?: ( chunk : string ) => void ;
2931 onTelemetry ?: ( telemetry : ParsedTelemetry ) => void ;
32+ onSessionId ?: ( sessionId : string ) => void ;
3033 abortSignal ?: AbortSignal ;
3134 timeout ?: number ; // Timeout in milliseconds (default: 1800000ms = 30 minutes)
3235}
@@ -38,6 +41,20 @@ export interface RunMistralResult {
3841
3942const ANSI_ESCAPE_SEQUENCE = new RegExp ( String . raw `\u001B\[[0-9;?]*[ -/]*[@-~]` , 'g' ) ;
4043
44+ /**
45+ * Build the final resume prompt combining steering instruction with user message
46+ */
47+ function buildResumePrompt ( userPrompt ?: string ) : string {
48+ const defaultPrompt = 'Continue from where you left off.' ;
49+
50+ if ( ! userPrompt ) {
51+ return defaultPrompt ;
52+ }
53+
54+ // Combine steering instruction with user's message
55+ return `[USER STEERING] The user paused this session to give you new direction. Continue from where you left off, but prioritize the user's request: "${ userPrompt } "` ;
56+ }
57+
4158// Track tool names for associating with results
4259const toolNameMap = new Map < string , string > ( ) ;
4360
@@ -48,51 +65,75 @@ function formatStreamJsonLine(line: string): string | null {
4865 try {
4966 const json = JSON . parse ( line ) ;
5067
51- // Mistral Vibe uses role/content format instead of type/message format
52- // Handle both formats for compatibility
5368 const role = json . role || json . type ;
5469 const content = json . content || json . message ?. content ;
5570
5671 if ( role === 'assistant' ) {
57- // Handle assistant messages
58- if ( typeof content === 'string' ) {
59- // Simple string content
72+ // Handle tool_calls array
73+ if ( Array . isArray ( json . tool_calls ) && json . tool_calls . length > 0 ) {
74+ const results : string [ ] = [ ] ;
75+ for ( const toolCall of json . tool_calls ) {
76+ const toolName = toolCall . function ?. name || toolCall . name || 'tool' ;
77+ const toolId = toolCall . id ;
78+ if ( toolId && toolName ) {
79+ toolNameMap . set ( toolId , toolName ) ;
80+ }
81+ results . push ( formatCommand ( toolName , 'started' ) ) ;
82+ }
83+ return results . join ( '\n' ) ;
84+ }
85+
86+ // Handle text content
87+ if ( typeof content === 'string' && content . trim ( ) ) {
6088 return content ;
6189 } else if ( Array . isArray ( content ) ) {
62- // Array of content blocks (like Claude/Gemini format)
6390 for ( const block of content ) {
6491 if ( block . type === 'text' && block . text ) {
6592 return block . text ;
6693 } else if ( block . type === 'thinking' && block . text ) {
6794 return formatThinking ( block . text ) ;
6895 } else if ( block . type === 'tool_use' ) {
69- // Track tool name for later use with result
7096 if ( block . id && block . name ) {
7197 toolNameMap . set ( block . id , block . name ) ;
7298 }
73- const commandName = block . name || 'tool' ;
74- return formatCommand ( commandName , 'started' ) ;
99+ return formatCommand ( block . name || 'tool' , 'started' ) ;
75100 }
76101 }
77102 }
103+ } else if ( role === 'tool' ) {
104+ // Handle tool results
105+ const toolName = json . name || ( json . tool_call_id ? toolNameMap . get ( json . tool_call_id ) : undefined ) || 'tool' ;
106+
107+ if ( json . tool_call_id ) {
108+ toolNameMap . delete ( json . tool_call_id ) ;
109+ }
110+
111+ let preview : string ;
112+ const toolContent = json . content ;
113+ if ( typeof toolContent === 'string' ) {
114+ const trimmed = toolContent . trim ( ) ;
115+ preview = trimmed
116+ ? ( trimmed . length > 100 ? trimmed . substring ( 0 , 100 ) + '...' : trimmed )
117+ : 'empty' ;
118+ } else {
119+ preview = JSON . stringify ( toolContent ) ;
120+ }
121+ return formatCommand ( toolName , 'success' ) + '\n' + formatResult ( preview , false ) ;
78122 } else if ( role === 'user' ) {
79- // Handle user messages ( tool results)
123+ // Handle user messages with tool results
80124 if ( Array . isArray ( content ) ) {
81125 for ( const block of content ) {
82126 if ( block . type === 'tool_result' ) {
83- // Get tool name from map
84127 const toolName = block . tool_use_id ? toolNameMap . get ( block . tool_use_id ) : undefined ;
85128 const commandName = toolName || 'tool' ;
86129
87- // Clean up the map entry
88130 if ( block . tool_use_id ) {
89131 toolNameMap . delete ( block . tool_use_id ) ;
90132 }
91133
92134 let preview : string ;
93135 if ( block . is_error ) {
94136 preview = typeof block . content === 'string' ? block . content : JSON . stringify ( block . content ) ;
95- // Show command in red with nested error
96137 return formatCommand ( commandName , 'error' ) + '\n' + formatResult ( preview , true ) ;
97138 } else {
98139 if ( typeof block . content === 'string' ) {
@@ -103,7 +144,6 @@ function formatStreamJsonLine(line: string): string | null {
103144 } else {
104145 preview = JSON . stringify ( block . content ) ;
105146 }
106- // Show command in green with nested result
107147 return formatCommand ( commandName , 'success' ) + '\n' + formatResult ( preview , false ) ;
108148 }
109149 }
@@ -134,7 +174,7 @@ function formatStreamJsonLine(line: string): string | null {
134174}
135175
136176export async function runMistral ( options : RunMistralOptions ) : Promise < RunMistralResult > {
137- const { prompt, workingDir, model, env, onData, onErrorData, onTelemetry, abortSignal, timeout = 1800000 } = options ;
177+ const { prompt, workingDir, resumeSessionId , resumePrompt , model, env, onData, onErrorData, onTelemetry, onSessionId , abortSignal, timeout = 1800000 } = options ;
138178
139179 if ( ! prompt ) {
140180 throw new Error ( 'runMistral requires a prompt.' ) ;
@@ -185,13 +225,16 @@ export async function runMistral(options: RunMistralOptions): Promise<RunMistral
185225 return result ;
186226 } ;
187227
188- const { command, args } = buildMistralExecCommand ( { workingDir, prompt, model } ) ;
228+ // When resuming, use the resume prompt instead of the original prompt
229+ const effectivePrompt = resumeSessionId ? buildResumePrompt ( resumePrompt ) : prompt ;
230+ const { command, args } = buildMistralExecCommand ( { workingDir, prompt : effectivePrompt , resumeSessionId, model } ) ;
189231
190232 // Create telemetry capture instance
191233 const telemetryCapture = createTelemetryCapture ( 'mistral' , model , prompt , workingDir ) ;
192234
193235 // Track JSON error events (Mistral may exit 0 even on errors)
194236 let capturedError : string | null = null ;
237+ let sessionIdCaptured = false ;
195238
196239 let result ;
197240 try {
@@ -218,6 +261,13 @@ export async function runMistral(options: RunMistralOptions): Promise<RunMistral
218261 // Check for error events (Mistral may exit 0 even on errors like invalid model)
219262 try {
220263 const json = JSON . parse ( line ) ;
264+
265+ // Capture session ID from first event that contains it
266+ if ( ! sessionIdCaptured && json . session_id && onSessionId ) {
267+ sessionIdCaptured = true ;
268+ onSessionId ( json . session_id ) ;
269+ }
270+
221271 // Check for error in result type
222272 if ( json . type === 'result' && json . is_error && json . result && ! capturedError ) {
223273 capturedError = json . result ;
0 commit comments