@@ -37,24 +37,13 @@ let isGenerating = false;
3737const conversation : ChatMessage [ ] = [ ] ;
3838const pendingToolCalls = new Map < string , ( result : unknown ) => void > ( ) ;
3939
40- const MODEL_ID = "Qwen3-0.6B-q4f16_1-MLC" ;
41-
42- const SYSTEM_PROMPT = `You are ModelScript AI, an expert Modelica language assistant integrated into the ModelScript IDE.
43-
44- You help users with:
45- - Writing and debugging Modelica models, packages, functions, and connectors
46- - Understanding the Modelica Standard Library (MSL) components
47- - Setting up simulations and interpreting results
48- - Explaining compiler diagnostics and suggesting fixes
40+ // Workspace context (updated automatically by extension host)
41+ let activeFileName : string | null = null ;
42+ let activeFileContent : string | null = null ;
4943
50- You have access to the following tools:
51- - modelscript_flatten: Flatten a Modelica class. Use: TOOL_CALL: {"tool":"modelscript_flatten","name":"ClassName"}
52- - modelscript_simulate: Simulate a model. Use: TOOL_CALL: {"tool":"modelscript_simulate","name":"ClassName"}
53- - modelscript_query: Inspect a class. Use: TOOL_CALL: {"tool":"modelscript_query","name":"ClassName"}
54- - modelscript_parse: Parse Modelica code. Use: TOOL_CALL: {"tool":"modelscript_parse","code":"model M end M;"}
44+ const MODEL_ID = "Qwen3-0.6B-q4f16_1-MLC" ;
5545
56- When referencing Modelica code, use proper syntax. Be concise and helpful.
57- Do not use <think> tags or internal reasoning blocks. Respond directly.` ;
46+ const SYSTEM_PROMPT = `You are ModelScript AI, a Modelica language assistant. Answer questions about Modelica code concisely. When the user provides code context, base your answer on that specific code. Do not use <think> tags.` ;
5847
5948// ── WebLLM Engine (runs in main thread, GPU inference in internal workers) ──
6049
@@ -129,7 +118,11 @@ function addMessage(role: "user" | "assistant" | "tool", content: string): HTMLE
129118}
130119
131120function stripThinkTags ( text : string ) : string {
132- return text . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > \s * / g, "" ) . trim ( ) ;
121+ // Strip complete <think>...</think> blocks
122+ let cleaned = text . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > \s * / g, "" ) ;
123+ // Strip incomplete <think> blocks (no closing tag, model was cut off)
124+ cleaned = cleaned . replace ( / < t h i n k > [ \s \S ] * / g, "" ) ;
125+ return cleaned . trim ( ) ;
133126}
134127
135128function formatContent ( text : string ) : string {
@@ -140,9 +133,42 @@ function formatContent(text: string): string {
140133 '<code style="background:var(--vscode-textCodeBlock-background,#1a1a1a);padding:1px 4px;border-radius:3px;">$1</code>' ,
141134 ) ;
142135 html = html . replace ( / \* \* ( [ ^ * ] + ) \* \* / g, "<strong>$1</strong>" ) ;
136+ // Display math: $$...$$
137+ html = html . replace ( / \$ \$ ( [ \s \S ] * ?) \$ \$ / g, ( _m , expr ) => `<div class="math-block">${ renderLatex ( expr ) } </div>` ) ;
138+ // Inline math: $...$
139+ html = html . replace ( / \$ ( [ ^ $ \n ] + ) \$ / g, ( _m , expr ) => `<span class="math-inline">${ renderLatex ( expr ) } </span>` ) ;
143140 return html ;
144141}
145142
143+ function renderLatex ( expr : string ) : string {
144+ let text = expr . trim ( ) ;
145+ // \frac{a}{b} → a/b
146+ text = text . replace ( / \\ f r a c \{ ( [ ^ } ] + ) \} \{ ( [ ^ } ] + ) \} / g, "($1)/($2)" ) ;
147+ // \text{...} → ...
148+ text = text . replace ( / \\ t e x t \{ ( [ ^ } ] + ) \} / g, "$1" ) ;
149+ // \cdot → ·
150+ text = text . replace ( / \\ c d o t / g, "·" ) ;
151+ // \times → ×
152+ text = text . replace ( / \\ t i m e s / g, "×" ) ;
153+ // \leq, \geq, \neq
154+ text = text . replace ( / \\ l e q / g, "≤" ) . replace ( / \\ g e q / g, "≥" ) . replace ( / \\ n e q / g, "≠" ) ;
155+ // \sum, \prod, \int
156+ text = text
157+ . replace ( / \\ s u m / g, "∑" )
158+ . replace ( / \\ p r o d / g, "∏" )
159+ . replace ( / \\ i n t / g, "∫" ) ;
160+ // \infty → ∞
161+ text = text . replace ( / \\ i n f t y / g, "∞" ) ;
162+ // \sqrt{x} → √(x)
163+ text = text . replace ( / \\ s q r t \{ ( [ ^ } ] + ) \} / g, "√($1)" ) ;
164+ // \partial → ∂
165+ text = text . replace ( / \\ p a r t i a l / g, "∂" ) ;
166+ // d(...)/dt style: keep as-is
167+ // Remove remaining backslashes from unknown commands
168+ text = text . replace ( / \\ ( [ a - z A - Z ] + ) / g, "$1" ) ;
169+ return text ;
170+ }
171+
146172function addTypingIndicator ( ) : HTMLElement {
147173 const div = document . createElement ( "div" ) ;
148174 div . className = "msg assistant typing" ;
@@ -172,12 +198,8 @@ window.addEventListener("message", (event) => {
172198 }
173199 break ;
174200 case "activeFileContext" :
175- if ( msg . content ) {
176- conversation . push ( {
177- role : "system" ,
178- content : `The user currently has the file "${ msg . fileName } " open:\n\`\`\`modelica\n${ msg . content } \n\`\`\`` ,
179- } ) ;
180- }
201+ activeFileName = msg . fileName ?? null ;
202+ activeFileContent = msg . content ?? null ;
181203 break ;
182204 }
183205} ) ;
@@ -193,9 +215,22 @@ async function sendMessage(): Promise<void> {
193215 inputEl . style . height = "36px" ;
194216 sendBtn . disabled = true ;
195217
218+ // Show what the user typed, but send augmented version with context to the model
196219 addMessage ( "user" , text ) ;
197- conversation . push ( { role : "user" , content : text } ) ;
198220
221+ // Build the augmented user message with workspace context prepended
222+ let augmentedText = "" ;
223+ if ( activeFileName && activeFileContent ) {
224+ const lines = activeFileContent . split ( "\n" ) ;
225+ const truncated = lines . length > 25 ? lines . slice ( 0 , 25 ) . join ( "\n" ) + "\n// ..." : activeFileContent ;
226+ augmentedText += `Here is the code from "${ activeFileName } " currently open in the editor:\n${ truncated } \n\n` ;
227+ }
228+ augmentedText += text ;
229+
230+ conversation . push ( { role : "user" , content : augmentedText } ) ;
231+ console . log ( "[chat] context:" , { activeFile : activeFileName , hasContent : ! ! activeFileContent } ) ;
232+
233+ // Build messages: short system prompt + conversation with augmented user messages
199234 const messages : ChatMessage [ ] = [ { role : "system" , content : SYSTEM_PROMPT } , ...conversation ] ;
200235
201236 const typingEl = addTypingIndicator ( ) ;
@@ -206,18 +241,66 @@ async function sendMessage(): Promise<void> {
206241 const completion = await engine . chat . completions . create ( {
207242 messages,
208243 temperature : 0.7 ,
209- max_tokens : 4096 ,
244+ max_tokens : 2048 ,
210245 stream : false ,
211246 } ) ;
212247
248+ let rawText = completion . choices ?. [ 0 ] ?. message ?. content ?? "" ;
249+ let finishReason = completion . choices ?. [ 0 ] ?. finish_reason ?? "stop" ;
250+ let visibleText = stripThinkTags ( rawText ) ;
251+
252+ // If the model only produced <think> content, retry once with a direct instruction
253+ if ( ! visibleText ) {
254+ statusEl . textContent = "Retrying..." ;
255+ const retryMessages = [
256+ { role : "system" as const , content : "Answer directly and concisely. No reasoning tags." } ,
257+ ...conversation ,
258+ ] ;
259+ const retry = await engine . chat . completions . create ( {
260+ messages : retryMessages ,
261+ temperature : 0.5 ,
262+ max_tokens : 2048 ,
263+ stream : false ,
264+ } ) ;
265+ rawText = retry . choices ?. [ 0 ] ?. message ?. content ?? "" ;
266+ finishReason = retry . choices ?. [ 0 ] ?. finish_reason ?? "stop" ;
267+ visibleText = stripThinkTags ( rawText ) ;
268+ }
269+
270+ // If truncated (finish_reason="length"), try one continuation with stripped content
271+ if ( finishReason === "length" && visibleText ) {
272+ statusEl . textContent = "Continuing..." ;
273+ const contMessages = [
274+ ...messages ,
275+ { role : "assistant" as const , content : visibleText } ,
276+ { role : "user" as const , content : "Continue." } ,
277+ ] ;
278+ const cont = await engine . chat . completions . create ( {
279+ messages : contMessages ,
280+ temperature : 0.7 ,
281+ max_tokens : 2048 ,
282+ stream : false ,
283+ } ) ;
284+ const contChunk = stripThinkTags ( cont . choices ?. [ 0 ] ?. message ?. content ?? "" ) ;
285+ if ( contChunk ) {
286+ visibleText += " " + contChunk ;
287+ rawText += cont . choices ?. [ 0 ] ?. message ?. content ?? "" ;
288+ }
289+ }
290+
213291 typingEl . remove ( ) ;
214- const resultText = completion . choices ?. [ 0 ] ?. message ?. content ?? " ";
292+ statusEl . textContent = "Qwen3-0.6B ready ";
215293
216- addMessage ( "assistant" , resultText ) ;
217- conversation . push ( { role : "assistant" , content : resultText } ) ;
294+ if ( visibleText ) {
295+ addMessage ( "assistant" , visibleText ) ;
296+ } else {
297+ addMessage ( "assistant" , "I couldn't generate a response. Try a shorter or more specific question." ) ;
298+ }
299+
300+ conversation . push ( { role : "assistant" , content : visibleText || rawText } ) ;
218301
219302 // Check for tool calls in the response
220- const toolCallMatch = resultText . match ( / T O O L _ C A L L : \s * ( \{ [ \s \S ] * ?\} ) / ) ;
303+ const toolCallMatch = visibleText . match ( / T O O L _ C A L L : \s * ( \{ [ \s \S ] * ?\} ) / ) ;
221304 if ( toolCallMatch ) {
222305 try {
223306 const toolReq = JSON . parse ( toolCallMatch [ 1 ] ) ;
@@ -279,3 +362,6 @@ inputEl.addEventListener("input", () => {
279362inputEl . disabled = false ;
280363sendBtn . disabled = false ;
281364inputEl . focus ( ) ;
365+
366+ // Request workspace context now that the script is loaded
367+ vscode . postMessage ( { type : "getActiveFileContext" } ) ;
0 commit comments