@@ -441,6 +441,41 @@ export class CanvasPanel implements vscode.Disposable {
441441 this . handleWebviewError ( message . payload ) ;
442442 break ;
443443
444+ case 'canvasAiChat' :
445+ await this . handleCanvasAiChat ( message . payload as {
446+ userInput : string ;
447+ images ?: string [ ] ;
448+ imageMetadata ?: { deviceMode : string ; deviceViewport : { width : number ; height : number } } ;
449+ context : {
450+ canvasId : string ;
451+ canvasName ?: string ;
452+ componentCount ?: number ;
453+ components : Array < {
454+ id : string ;
455+ name ?: string ;
456+ folderPath ?: string ;
457+ entryFile ?: string ;
458+ } > ;
459+ selectedComponent ?: {
460+ id : string ;
461+ name ?: string ;
462+ folderPath ?: string ;
463+ entryFile ?: string ;
464+ } ;
465+ selectedElement ?: {
466+ componentId : string ;
467+ sourceLocation ?: {
468+ file : string ;
469+ startLine : number ;
470+ endLine ?: number ;
471+ column ?: number ;
472+ } ;
473+ } ;
474+ } ;
475+ autoSend ?: boolean ;
476+ } ) ;
477+ break ;
478+
444479 default :
445480 this . logger . warn ( `Unknown webview message type: ${ message . type } ` ) ;
446481 }
@@ -452,6 +487,102 @@ export class CanvasPanel implements vscode.Disposable {
452487 }
453488 }
454489
490+ private async handleCanvasAiChat ( payload : {
491+ userInput : string ;
492+ images ?: string [ ] ;
493+ imageMetadata ?: { deviceMode : string ; deviceViewport : { width : number ; height : number } } ;
494+ context : {
495+ canvasId : string ;
496+ canvasName ?: string ;
497+ componentCount ?: number ;
498+ components : Array < {
499+ id : string ;
500+ name ?: string ;
501+ folderPath ?: string ;
502+ entryFile ?: string ;
503+ } > ;
504+ selectedComponent ?: {
505+ id : string ;
506+ name ?: string ;
507+ folderPath ?: string ;
508+ entryFile ?: string ;
509+ } ;
510+ selectedElement ?: {
511+ componentId : string ;
512+ sourceLocation ?: {
513+ file : string ;
514+ startLine : number ;
515+ endLine ?: number ;
516+ column ?: number ;
517+ } ;
518+ } ;
519+ } ;
520+ autoSend ?: boolean ;
521+ } ) : Promise < void > {
522+ const userInput = payload ?. userInput ?. trim ( ) ?? '' ;
523+ const images = payload ?. images ;
524+ const imageMetadata = payload ?. imageMetadata ;
525+ if ( ! userInput && ( ! images || images . length === 0 ) ) {
526+ return ;
527+ }
528+
529+ const context = payload . context ;
530+ const lines : string [ ] = [ ] ;
531+ lines . push ( '[Roopik Canvas Context]' ) ;
532+ lines . push ( `canvasId: ${ context . canvasId } ` ) ;
533+ if ( context . canvasName ) {
534+ lines . push ( `canvasName: ${ context . canvasName } ` ) ;
535+ }
536+
537+ if ( context . selectedComponent ) {
538+ lines . push ( 'Selected component:' ) ;
539+ lines . push ( `- id: ${ context . selectedComponent . id } ` ) ;
540+ if ( context . selectedComponent . name ) {
541+ lines . push ( `- name: ${ context . selectedComponent . name } ` ) ;
542+ }
543+ if ( context . selectedComponent . folderPath ) {
544+ lines . push ( `- folderPath: ${ context . selectedComponent . folderPath } ` ) ;
545+ }
546+ if ( context . selectedComponent . entryFile ) {
547+ const entryPath = context . selectedComponent . folderPath
548+ ? path . join ( context . selectedComponent . folderPath , context . selectedComponent . entryFile )
549+ : context . selectedComponent . entryFile ;
550+ lines . push ( `- entryFile: ${ entryPath } ` ) ;
551+ }
552+ }
553+
554+ if ( context . selectedElement ?. sourceLocation ) {
555+ const source = context . selectedElement . sourceLocation ;
556+ const lineRange = source . endLine && source . endLine !== source . startLine
557+ ? `${ source . startLine } -${ source . endLine } `
558+ : `${ source . startLine } ` ;
559+ lines . push ( 'Selected element:' ) ;
560+ lines . push ( `- componentId: ${ context . selectedElement . componentId } ` ) ;
561+ lines . push ( `- source: ${ source . file } :${ lineRange } ` ) ;
562+ }
563+
564+ lines . push ( '' ) ;
565+ if ( images && images . length > 0 && imageMetadata ) {
566+ lines . push ( `Device mode: ${ imageMetadata . deviceMode } ` ) ;
567+ lines . push ( `Device viewport: ${ imageMetadata . deviceViewport . width } x${ imageMetadata . deviceViewport . height } ` ) ;
568+ }
569+
570+ if ( userInput ) {
571+ lines . push ( 'User request:' ) ;
572+ lines . push ( userInput ) ;
573+ }
574+
575+ try {
576+ await vscode . commands . executeCommand ( 'roodio.externalContext' , {
577+ promptText : lines . join ( '\n' ) ,
578+ images,
579+ autoSend : payload . autoSend === true ,
580+ } ) ;
581+ } catch ( error ) {
582+ this . logger . error ( `Failed to forward canvas AI context: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
583+ }
584+ }
585+
455586 /**
456587 * Handle drop component request from webview (drag-drop from OS file manager)
457588 *
@@ -669,7 +800,9 @@ export class CanvasPanel implements vscode.Disposable {
669800 this . loadedComponents . push ( {
670801 componentId,
671802 contentHash : ref . contentHash ,
672- name : ref . componentName
803+ name : ref . componentName ,
804+ folderPath : ref . folderPath ,
805+ entryFile : ref . entryFile
673806 } ) ;
674807 }
675808 }
@@ -720,13 +853,15 @@ export class CanvasPanel implements vscode.Disposable {
720853 this . logger . info ( `Loading ${ this . loadedComponents . length } existing components` ) ;
721854
722855 for ( const component of this . loadedComponents ) {
723- const { componentId, contentHash, name } = component ;
856+ const { componentId, contentHash, name, folderPath , entryFile } = component ;
724857
725858 // 1. Notify webview that component exists (shows loading spinner)
726859 this . postToWebview ( 'componentCreated' , {
727860 componentId,
728861 canvasId : this . canvasId ,
729- name
862+ name,
863+ folderPath,
864+ entryFile
730865 } ) ;
731866
732867 // 2. Load component (checks cache, rebuilds if needed)
0 commit comments