11// SPDX-License-Identifier: AGPL-3.0-or-later
22//
3- // Manages the AI Chat webview panel lifecycle .
4- // The webview runs WebLLM (Qwen3.5 ) directly via WebGPU.
3+ // Provides the AI Chat as a WebviewViewProvider (secondary sidebar) .
4+ // The webview runs WebLLM (Qwen3) directly via WebGPU.
55// The extension host bridges tool calls to the LSP server.
66
77import * as vscode from "vscode" ;
88import { LanguageClient } from "vscode-languageclient/browser" ;
99
10- export class ChatPanel {
11- static currentPanel : ChatPanel | undefined ;
10+ export class ChatViewProvider implements vscode . WebviewViewProvider {
1211 static readonly viewType = "modelscript.chat" ;
1312
14- private readonly panel : vscode . WebviewPanel ;
15- private readonly extensionUri : vscode . Uri ;
16- private readonly client : LanguageClient ;
13+ private view ?: vscode . WebviewView ;
1714 private disposables : vscode . Disposable [ ] = [ ] ;
1815
19- static createOrShow ( extensionUri : vscode . Uri , client : LanguageClient ) {
20- if ( ChatPanel . currentPanel ) {
21- ChatPanel . currentPanel . panel . reveal ( vscode . ViewColumn . Beside ) ;
22- return ;
23- }
16+ constructor (
17+ private readonly extensionUri : vscode . Uri ,
18+ private readonly client : LanguageClient ,
19+ ) { }
2420
25- const panel = vscode . window . createWebviewPanel ( ChatPanel . viewType , "ModelScript Chat" , vscode . ViewColumn . Beside , {
26- enableScripts : true ,
27- retainContextWhenHidden : true ,
28- localResourceRoots : [ vscode . Uri . joinPath ( extensionUri , "dist" ) ] ,
29- } ) ;
30-
31- ChatPanel . currentPanel = new ChatPanel ( panel , extensionUri , client ) ;
32- }
21+ resolveWebviewView ( webviewView : vscode . WebviewView ) : void {
22+ this . view = webviewView ;
3323
34- private constructor ( panel : vscode . WebviewPanel , extensionUri : vscode . Uri , client : LanguageClient ) {
35- this . panel = panel ;
36- this . extensionUri = extensionUri ;
37- this . client = client ;
24+ webviewView . webview . options = {
25+ enableScripts : true ,
26+ localResourceRoots : [
27+ vscode . Uri . joinPath ( this . extensionUri , "dist" ) ,
28+ vscode . Uri . joinPath ( this . extensionUri , "images" ) ,
29+ ] ,
30+ } ;
3831
39- this . panel . iconPath = vscode . Uri . joinPath ( extensionUri , "images" , "icon.png" ) ;
40- this . panel . webview . html = this . getHtmlForWebview ( ) ;
32+ webviewView . webview . html = this . getHtmlForWebview ( webviewView . webview ) ;
4133
4234 // Handle messages from webview (tool calls bridged to LSP)
43- this . panel . webview . onDidReceiveMessage (
35+ webviewView . webview . onDidReceiveMessage (
4436 async ( msg ) => {
4537 switch ( msg . type ) {
4638 case "toolCall" :
@@ -58,7 +50,7 @@ export class ChatPanel {
5850 this . disposables ,
5951 ) ;
6052
61- // Auto-send active file context when panel opens
53+ // Auto-send active file context when view opens
6254 this . sendActiveFileContext ( ) ;
6355
6456 // Re-send active file context when user switches editors
@@ -68,10 +60,16 @@ export class ChatPanel {
6860 } ) ,
6961 ) ;
7062
71- this . panel . onDidDispose ( ( ) => this . dispose ( ) , null , this . disposables ) ;
63+ webviewView . onDidDispose ( ( ) => {
64+ while ( this . disposables . length ) {
65+ const x = this . disposables . pop ( ) ;
66+ if ( x ) x . dispose ( ) ;
67+ }
68+ } ) ;
7269 }
7370
7471 private async handleToolCall ( msg : { id : string ; tool : string ; input : Record < string , unknown > } ) {
72+ if ( ! this . view ) return ;
7573 try {
7674 let result : unknown ;
7775 switch ( msg . tool ) {
@@ -90,9 +88,9 @@ export class ChatPanel {
9088 default :
9189 result = { error : `Unknown tool: ${ msg . tool } ` } ;
9290 }
93- this . panel . webview . postMessage ( { type : "toolResult" , id : msg . id , result } ) ;
91+ this . view . webview . postMessage ( { type : "toolResult" , id : msg . id , result } ) ;
9492 } catch ( e ) {
95- this . panel . webview . postMessage ( {
93+ this . view . webview . postMessage ( {
9694 type : "toolResult" ,
9795 id : msg . id ,
9896 result : { error : e instanceof Error ? e . message : String ( e ) } ,
@@ -101,13 +99,14 @@ export class ChatPanel {
10199 }
102100
103101 private async handleListClasses ( msg : { id : string } ) {
102+ if ( ! this . view ) return ;
104103 try {
105104 const result = ( await this . client . sendRequest ( "modelscript/listClasses" ) ) as {
106105 classes : { name : string ; kind : string ; uri : string } [ ] ;
107106 } ;
108- this . panel . webview . postMessage ( { type : "classListResult" , id : msg . id , result } ) ;
107+ this . view . webview . postMessage ( { type : "classListResult" , id : msg . id , result } ) ;
109108 } catch ( e ) {
110- this . panel . webview . postMessage ( {
109+ this . view . webview . postMessage ( {
111110 type : "classListResult" ,
112111 id : msg . id ,
113112 result : { classes : [ ] , error : e instanceof Error ? e . message : String ( e ) } ,
@@ -116,21 +115,20 @@ export class ChatPanel {
116115 }
117116
118117 private sendActiveFileContext ( ) {
119- // activeTextEditor is undefined when the webview panel has focus,
120- // so fall back to visibleTextEditors to find any open Modelica file.
118+ if ( ! this . view ) return ;
121119 let editor = vscode . window . activeTextEditor ;
122120 if ( ! editor || editor . document . languageId !== "modelica" ) {
123121 editor = vscode . window . visibleTextEditors . find ( ( e ) => e . document . languageId === "modelica" ) ;
124122 }
125123 if ( editor && editor . document . languageId === "modelica" ) {
126- this . panel . webview . postMessage ( {
124+ this . view . webview . postMessage ( {
127125 type : "activeFileContext" ,
128126 fileName : editor . document . fileName . split ( "/" ) . pop ( ) ,
129127 content : editor . document . getText ( ) ,
130128 uri : editor . document . uri . toString ( ) ,
131129 } ) ;
132130 } else {
133- this . panel . webview . postMessage ( {
131+ this . view . webview . postMessage ( {
134132 type : "activeFileContext" ,
135133 fileName : null ,
136134 content : null ,
@@ -139,22 +137,20 @@ export class ChatPanel {
139137 }
140138 }
141139
142- private getHtmlForWebview ( ) : string {
143- const webview = this . panel . webview ;
140+ private getHtmlForWebview ( webview : vscode . Webview ) : string {
144141 const scriptUri = webview . asWebviewUri ( vscode . Uri . joinPath ( this . extensionUri , "dist" , "chatWebview.js" ) ) ;
145142 const nonce = getNonce ( ) ;
146143
147- // Model files are served by the IDE Express server at /api/models/.
148- // We need the server origin (not the webview's virtual origin) for fetch.
149- // The scriptUri gives us the server origin.
144+ const iconUri = webview . asWebviewUri ( vscode . Uri . joinPath ( this . extensionUri , "images" , "icon.png" ) ) ;
145+
150146 const serverOrigin = new URL ( scriptUri . toString ( ) ) . origin ;
151147 const modelBaseUrl = `${ serverOrigin } /api/models` ;
152148
153- // CSP: WebLLM runs in the webview main thread, needs WASM + fetch to model server
154149 const csp = [
155150 "default-src 'none'" ,
156151 `style-src 'unsafe-inline'` ,
157152 `script-src 'nonce-${ nonce } ' 'wasm-unsafe-eval'` ,
153+ `img-src ${ webview . cspSource } ` ,
158154 `connect-src http: https:` ,
159155 ] . join ( ";" ) ;
160156
@@ -330,7 +326,7 @@ export class ChatPanel {
330326 border-color: var(--vscode-focusBorder, #0078d4);
331327 }
332328 #send-btn {
333- align-self: flex-end ;
329+ align-self: center ;
334330 background: var(--vscode-button-background, #0078d4);
335331 color: var(--vscode-button-foreground, #fff);
336332 border: none;
@@ -347,11 +343,29 @@ export class ChatPanel {
347343 opacity: 0.5;
348344 cursor: not-allowed;
349345 }
346+
347+ /* Empty state: center input vertically, stack send button below */
348+ body.empty #messages { display: none; }
349+ body.empty {
350+ justify-content: center;
351+ }
352+ body.empty #input-area {
353+ flex-direction: column;
354+ border-top: none;
355+ padding: 16px 24px;
356+ }
357+ body.empty #input {
358+ min-height: 60px;
359+ }
360+ body.empty #send-btn {
361+ align-self: stretch;
362+ padding: 10px;
363+ }
350364 </style>
351365</head>
352- <body>
366+ <body class="empty" >
353367 <div id="header">
354- <span>🤖 ModelScript AI</span>
368+ <span><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmodelscript%2Fmodelscript%2Fcommit%2F%3C%2Fspan%3E%3Cspan+class%3D"pl-s1">${ iconUri } " width="16" height="16" style="vertical-align: middle; margin-right: 4px;"> ModelScript AI</span>
355369 <span class="status" id="model-status">Ready</span>
356370 </div>
357371 <div id="progress-container">
@@ -370,15 +384,6 @@ export class ChatPanel {
370384</body>
371385</html>` ;
372386 }
373-
374- dispose ( ) {
375- ChatPanel . currentPanel = undefined ;
376- this . panel . dispose ( ) ;
377- while ( this . disposables . length ) {
378- const x = this . disposables . pop ( ) ;
379- if ( x ) x . dispose ( ) ;
380- }
381- }
382387}
383388
384389function getNonce ( ) {
0 commit comments