11import * as vscode from 'vscode' ;
22import { getAllUncommittedFileDiffs , UncommittedDiffResult } from '../services/gitService' ;
3- import { postFileDiff } from '../services/networkClient' ;
3+ import { postFileDiff , type TokenScore } from '../services/networkClient' ;
44
55export class CuratorViewProvider implements vscode . WebviewViewProvider {
66 public static readonly viewType = 'curatorView' ;
77
88 private _view : vscode . WebviewView | undefined ;
99 private readonly _extensionUri : vscode . Uri ;
1010 private _lastDiffs : UncommittedDiffResult [ ] = [ ] ;
11+ private _lastTokenScoresByFile : Map < string , TokenScore [ ] > = new Map ( ) ;
1112
1213 constructor ( extensionUri : vscode . Uri ) {
1314 this . _extensionUri = extensionUri ;
@@ -31,6 +32,13 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
3132 console . log ( '[Curator] Failed to open file' , e ) ;
3233 }
3334 }
35+ } else if ( msg ?. type === 'openHtml' && typeof msg . filename === 'string' ) {
36+ const tokens = this . _lastTokenScoresByFile . get ( msg . filename ) ;
37+ if ( tokens && tokens . length > 0 ) {
38+ await this . openHtmlTokenView ( msg . filename , tokens ) ;
39+ } else {
40+ void vscode . window . showWarningMessage ( 'Curator: No tokens available for ' + msg . filename ) ;
41+ }
3442 }
3543 } ) ;
3644 }
@@ -70,9 +78,9 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
7078 .tooltip.visible { opacity: 1; }
7179
7280 .actions { display: inline-flex; align-items: center; gap: 6px; }
73- .open-file { background: none; border: none; padding: 2px; cursor: pointer; color: var(--vscode-foreground); opacity: 0.8; }
74- .open-file:hover { opacity: 1; }
75- .open-file svg { width: 14px; height: 14px; display: block; fill: currentColor; }
81+ .open-file, .open-html { background: none; border: none; padding: 2px; cursor: pointer; color: var(--vscode-foreground); opacity: 0.8; }
82+ .open-file:hover, .open-html:hover { opacity: 1; }
83+ .open-file svg, .open-html svg { width: 14px; height: 14px; display: block; fill: currentColor; }
7684
7785 .progress { margin-top: 6px; font-size: 12px; color: var(--vscode-descriptionForeground); display: flex; align-items: center; gap: 8px; }
7886 .progress .bar { flex: 1; height: 6px; background: var(--vscode-editorWidget-border); border-radius: 3px; overflow: hidden; }
@@ -127,6 +135,7 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
127135 const doc = await vscode . workspace . openTextDocument ( diffResult . fileUri ) ;
128136 const fileText = doc . getText ( ) ;
129137 const response = await postFileDiff ( diffResult . diffText , fileText ) ;
138+ this . _lastTokenScoresByFile . set ( diffResult . relativePath , response . tokenScores ) ;
130139 await view . webview . postMessage ( { type : 'renderFile' , filename : diffResult . relativePath , tokenScores : response . tokenScores } ) ;
131140 } catch ( err ) {
132141 const message = err instanceof Error ? err . message : 'Failed to analyze diff.' ;
@@ -142,6 +151,56 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
142151 await view . webview . postMessage ( { type : 'loading' , value : false } ) ;
143152 }
144153 }
154+
155+ private async openHtmlTokenView ( title : string , tokenScores : TokenScore [ ] ) : Promise < void > {
156+ const panel = vscode . window . createWebviewPanel ( 'curatorTokens' , `Curator: ${ title } ` , vscode . ViewColumn . Active , { enableScripts : true , retainContextWhenHidden : false } ) ;
157+ const jsUri = panel . webview . asWebviewUri ( vscode . Uri . joinPath ( this . _extensionUri , 'media' , 'tokenView.js' ) ) ;
158+ const csp = `default-src 'none'; style-src ${ panel . webview . cspSource } 'unsafe-inline'; script-src ${ panel . webview . cspSource } ;` ;
159+ const safeJson = JSON . stringify ( tokenScores )
160+ . replace ( / < / g, '\\u003c' )
161+ . replace ( / > / g, '\\u003e' )
162+ . replace ( / & / g, '\\u0026' ) ;
163+ panel . webview . html = `<!DOCTYPE html>
164+ <html lang="en">
165+ <head>
166+ <meta charset="UTF-8" />
167+ <meta http-equiv="Content-Security-Policy" content="${ csp } " />
168+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
169+ <title>Curator: ${ this . escapeHtml ( title ) } </title>
170+ <style>
171+ body { font-family: var(--vscode-font-family); margin: 0; padding: 12px; }
172+ .code { white-space: pre-wrap; word-wrap: break-word; line-height: 1.6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12.5px; }
173+ .tok { display: inline; padding: 1px 2px; border-radius: 3px; color: #fff; }
174+ .tooltip { position: fixed; z-index: 1000; background: var(--vscode-editorWidget-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-editorWidget-border); border-radius: 4px; padding: 6px 8px; font-size: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); pointer-events: none; opacity: 0; transition: opacity 0.05s ease-in-out; max-width: 360px; white-space: pre-wrap; }
175+ .tooltip.visible { opacity: 1; }
176+ </style>
177+ </head>
178+ <body>
179+ <div class="code" id="code"></div>
180+ <div id="tooltip" class="tooltip" aria-hidden="true"></div>
181+ <script id="tokenData" type="application/json">${ safeJson } </script>
182+ <script src="${ jsUri } "></script>
183+ </body>
184+ </html>` ;
185+ }
186+
187+ private generateNonce ( ) : string {
188+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
189+ let out = '' ;
190+ for ( let i = 0 ; i < 16 ; i ++ ) {
191+ out += chars . charAt ( Math . floor ( Math . random ( ) * chars . length ) ) ;
192+ }
193+ return out ;
194+ }
195+
196+ private escapeHtml ( text : string ) : string {
197+ return String ( text )
198+ . replace ( / & / g, '&' )
199+ . replace ( / < / g, '<' )
200+ . replace ( / > / g, '>' )
201+ . replace ( / " / g, '"' )
202+ . replace ( / ' / g, ''' ) ;
203+ }
145204}
146205
147206
0 commit comments