1- import { randomBytes } from "node:crypto " ;
1+ import * as vscode from "vscode " ;
22
33import { type CoderApi } from "../../api/coderApi" ;
44import { type Logger } from "../../logging/logger" ;
5-
6- import type * as vscode from "vscode" ;
5+ import { getNonce } from "../util" ;
76
87/**
98 * Provides a webview that embeds the Coder agent chat UI.
@@ -34,15 +33,38 @@ export class ChatPanelProvider
3433 private readonly logger : Logger ,
3534 ) { }
3635
36+ private getTheme ( ) : "light" | "dark" {
37+ const kind = vscode . window . activeColorTheme . kind ;
38+ return kind === vscode . ColorThemeKind . Light ||
39+ kind === vscode . ColorThemeKind . HighContrastLight
40+ ? "light"
41+ : "dark" ;
42+ }
43+
44+ private sendScrollToBottom ( ) : void {
45+ this . view ?. webview . postMessage ( { type : "coder:scroll-to-bottom" } ) ;
46+ }
47+
48+ private sendTheme ( ) : void {
49+ this . view ?. webview . postMessage ( {
50+ type : "coder:set-theme" ,
51+ theme : this . getTheme ( ) ,
52+ } ) ;
53+ }
54+
3755 /**
3856 * Opens the chat panel for the given chat ID.
3957 * Called after a deep link reload via the persisted
4058 * pendingChatId, or directly for testing.
4159 */
4260 public openChat ( chatId : string ) : void {
61+ if ( this . chatId === chatId && this . view ) {
62+ this . view . show ( true ) ;
63+ return ;
64+ }
4365 this . chatId = chatId ;
4466 this . refresh ( ) ;
45- this . view ?. show ( true ) ;
67+ void vscode . commands . executeCommand ( ` ${ ChatPanelProvider . viewType } .focus` ) ;
4668 }
4769
4870 resolveWebviewView (
@@ -56,9 +78,12 @@ export class ChatPanelProvider
5678 webviewView . webview . onDidReceiveMessage ( ( msg : unknown ) => {
5779 this . handleMessage ( msg ) ;
5880 } ) ,
81+ vscode . window . onDidChangeActiveColorTheme ( ( ) => {
82+ this . sendTheme ( ) ;
83+ } ) ,
5984 ) ;
6085 this . renderView ( ) ;
61- webviewView . onDidDispose ( ( ) => this . dispose ( ) ) ;
86+ this . disposables . push ( webviewView . onDidDispose ( ( ) => this . dispose ( ) ) ) ;
6287 }
6388
6489 public refresh ( ) : void {
@@ -85,17 +110,33 @@ export class ChatPanelProvider
85110 return ;
86111 }
87112
88- const embedUrl = `${ coderUrl } /agents/${ this . chatId } /embed` ;
113+ const embedUrl = `${ coderUrl } /agents/${ this . chatId } /embed?theme= ${ this . getTheme ( ) } ` ;
89114 webview . html = this . getIframeHtml ( embedUrl , coderUrl ) ;
90115 }
91116
92117 private handleMessage ( message : unknown ) : void {
93118 if ( typeof message !== "object" || message === null ) {
94119 return ;
95120 }
96- const msg = message as { type ?: string } ;
97- if ( msg . type === "coder:vscode-ready" ) {
98- this . sendAuthToken ( ) ;
121+ const msg = message as { type ?: string ; payload ?: { url ?: string } } ;
122+ switch ( msg . type ) {
123+ case "coder:vscode-ready" :
124+ this . sendAuthToken ( ) ;
125+ break ;
126+ case "coder:chat-ready" :
127+ this . sendTheme ( ) ;
128+ this . sendScrollToBottom ( ) ;
129+ break ;
130+ case "coder:navigate" : {
131+ const url = msg . payload ?. url ;
132+ const coderUrl = this . client . getHost ( ) ;
133+ if ( url && coderUrl ) {
134+ void vscode . env . openExternal ( vscode . Uri . parse ( coderUrl + url ) ) ;
135+ }
136+ break ;
137+ }
138+ default :
139+ break ;
99140 }
100141 }
101142
@@ -142,7 +183,7 @@ export class ChatPanelProvider
142183 }
143184
144185 private getIframeHtml ( embedUrl : string , allowedOrigin : string ) : string {
145- const nonce = randomBytes ( 16 ) . toString ( "base64" ) ;
186+ const nonce = getNonce ( ) ;
146187
147188 return /* html */ `<!DOCTYPE html>
148189<html lang="en">
@@ -205,6 +246,12 @@ export class ChatPanelProvider
205246 status.textContent = 'Authenticating…';
206247 vscode.postMessage({ type: 'coder:vscode-ready' });
207248 }
249+ if (data.type === 'coder:chat-ready') {
250+ vscode.postMessage({ type: 'coder:chat-ready' });
251+ }
252+ if (data.type === 'coder:navigate') {
253+ vscode.postMessage(data);
254+ }
208255 return;
209256 }
210257
@@ -216,6 +263,18 @@ export class ChatPanelProvider
216263 }, '${ allowedOrigin } ');
217264 }
218265
266+ if (data.type === 'coder:set-theme') {
267+ iframe.contentWindow.postMessage({
268+ type: 'coder:set-theme',
269+ payload: { theme: data.theme },
270+ }, '${ allowedOrigin } ');
271+ }
272+
273+ if (data.type === 'coder:scroll-to-bottom') {
274+ iframe.contentWindow.postMessage(
275+ { type: 'coder:scroll-to-bottom' }, '${ allowedOrigin } ');
276+ }
277+
219278 if (data.type === 'coder:auth-error') {
220279 status.textContent = '';
221280 status.appendChild(document.createTextNode(data.error || 'Authentication failed.'));
0 commit comments