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,14 @@ 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 (
87+ webviewView . onDidDispose ( ( ) => this . dispose ( ) ) ,
88+ ) ;
6289 }
6390
6491 public refresh ( ) : void {
@@ -85,17 +112,35 @@ export class ChatPanelProvider
85112 return ;
86113 }
87114
88- const embedUrl = `${ coderUrl } /agents/${ this . chatId } /embed` ;
115+ const embedUrl = `${ coderUrl } /agents/${ this . chatId } /embed?theme= ${ this . getTheme ( ) } ` ;
89116 webview . html = this . getIframeHtml ( embedUrl , coderUrl ) ;
90117 }
91118
92119 private handleMessage ( message : unknown ) : void {
93120 if ( typeof message !== "object" || message === null ) {
94121 return ;
95122 }
96- const msg = message as { type ?: string } ;
97- if ( msg . type === "coder:vscode-ready" ) {
98- this . sendAuthToken ( ) ;
123+ const msg = message as { type ?: string ; payload ?: { url ?: string } } ;
124+ switch ( msg . type ) {
125+ case "coder:vscode-ready" :
126+ this . sendAuthToken ( ) ;
127+ break ;
128+ case "coder:chat-ready" :
129+ this . sendTheme ( ) ;
130+ this . sendScrollToBottom ( ) ;
131+ break ;
132+ case "coder:navigate" : {
133+ const url = msg . payload ?. url ;
134+ const coderUrl = this . client . getHost ( ) ;
135+ if ( url && coderUrl ) {
136+ void vscode . env . openExternal (
137+ vscode . Uri . parse ( coderUrl + url ) ,
138+ ) ;
139+ }
140+ break ;
141+ }
142+ default :
143+ break ;
99144 }
100145 }
101146
@@ -142,7 +187,7 @@ export class ChatPanelProvider
142187 }
143188
144189 private getIframeHtml ( embedUrl : string , allowedOrigin : string ) : string {
145- const nonce = randomBytes ( 16 ) . toString ( "base64" ) ;
190+ const nonce = getNonce ( ) ;
146191
147192 return /* html */ `<!DOCTYPE html>
148193<html lang="en">
@@ -205,6 +250,12 @@ export class ChatPanelProvider
205250 status.textContent = 'Authenticating…';
206251 vscode.postMessage({ type: 'coder:vscode-ready' });
207252 }
253+ if (data.type === 'coder:chat-ready') {
254+ vscode.postMessage({ type: 'coder:chat-ready' });
255+ }
256+ if (data.type === 'coder:navigate') {
257+ vscode.postMessage(data);
258+ }
208259 return;
209260 }
210261
@@ -216,6 +267,18 @@ export class ChatPanelProvider
216267 }, '${ allowedOrigin } ');
217268 }
218269
270+ if (data.type === 'coder:set-theme') {
271+ iframe.contentWindow.postMessage({
272+ type: 'coder:set-theme',
273+ payload: { theme: data.theme },
274+ }, '${ allowedOrigin } ');
275+ }
276+
277+ if (data.type === 'coder:scroll-to-bottom') {
278+ iframe.contentWindow.postMessage(
279+ { type: 'coder:scroll-to-bottom' }, '${ allowedOrigin } ');
280+ }
281+
219282 if (data.type === 'coder:auth-error') {
220283 status.textContent = '';
221284 status.appendChild(document.createTextNode(data.error || 'Authentication failed.'));
0 commit comments