@@ -163,6 +163,7 @@ export class PreviewPanel implements Disposable {
163163 content : msg . toTextContent ( ) ,
164164 } ) ) ;
165165 const rawJson = escapeHtml ( JSON . stringify ( wireMessages , null , 2 ) ) ;
166+ const frontmatterHtml = buildFrontmatterHtml ( agent ) ;
166167
167168 return /*html*/ `<!DOCTYPE html>
168169<html lang="en">
@@ -333,6 +334,136 @@ export class PreviewPanel implements Disposable {
333334 word-break: break-all;
334335 tab-size: 2;
335336 }
337+ .fm-grid {
338+ display: grid;
339+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
340+ gap: 8px;
341+ margin-bottom: 12px;
342+ }
343+ .fm-card, .fm-section {
344+ border: 1px solid var(--vscode-panel-border);
345+ border-radius: 6px;
346+ background: color-mix(in srgb, var(--vscode-editorWidget-background) 70%, transparent);
347+ }
348+ .fm-card {
349+ padding: 10px 12px;
350+ }
351+ .fm-card-label, .fm-section h3 {
352+ color: var(--vscode-descriptionForeground);
353+ font-size: 11px;
354+ font-weight: 600;
355+ letter-spacing: 0.5px;
356+ text-transform: uppercase;
357+ }
358+ .fm-card-label {
359+ margin-bottom: 4px;
360+ }
361+ .fm-card-value {
362+ font-size: 13px;
363+ word-break: break-word;
364+ }
365+ .fm-section {
366+ margin-bottom: 12px;
367+ padding: 10px 12px;
368+ }
369+ .fm-section h3 {
370+ margin: 0 0 8px 0;
371+ }
372+ .fm-description {
373+ margin: 0 0 12px 0;
374+ color: var(--vscode-descriptionForeground);
375+ }
376+ .fm-table {
377+ width: 100%;
378+ border-collapse: collapse;
379+ font-size: 12px;
380+ }
381+ .fm-table th {
382+ text-align: left;
383+ color: var(--vscode-descriptionForeground);
384+ font-weight: 600;
385+ border-bottom: 1px solid var(--vscode-panel-border);
386+ padding: 4px 6px;
387+ }
388+ .fm-table td {
389+ border-bottom: 1px solid color-mix(in srgb, var(--vscode-panel-border) 60%, transparent);
390+ padding: 5px 6px;
391+ vertical-align: top;
392+ word-break: break-word;
393+ }
394+ .fm-table tr:last-child td { border-bottom: 0; }
395+ .fm-chip {
396+ display: inline-block;
397+ background: var(--vscode-badge-background);
398+ color: var(--vscode-badge-foreground);
399+ border-radius: 3px;
400+ padding: 1px 6px;
401+ margin: 0 4px 4px 0;
402+ font-size: 11px;
403+ }
404+ .fm-property-list {
405+ display: flex;
406+ flex-direction: column;
407+ gap: 8px;
408+ }
409+ .fm-property {
410+ border: 1px solid color-mix(in srgb, var(--vscode-panel-border) 75%, transparent);
411+ border-radius: 5px;
412+ padding: 8px 10px;
413+ background: color-mix(in srgb, var(--vscode-editor-background) 55%, transparent);
414+ }
415+ .fm-property-head {
416+ display: flex;
417+ align-items: center;
418+ flex-wrap: wrap;
419+ gap: 6px;
420+ margin-bottom: 5px;
421+ }
422+ .fm-property-name {
423+ font-weight: 600;
424+ font-size: 13px;
425+ }
426+ .fm-kind, .fm-required, .fm-optional {
427+ border-radius: 3px;
428+ padding: 1px 6px;
429+ font-size: 10px;
430+ text-transform: uppercase;
431+ letter-spacing: 0.4px;
432+ }
433+ .fm-kind {
434+ background: color-mix(in srgb, var(--vscode-textLink-foreground) 18%, transparent);
435+ color: var(--vscode-textLink-foreground);
436+ }
437+ .fm-required {
438+ background: color-mix(in srgb, var(--vscode-errorForeground) 14%, transparent);
439+ color: var(--vscode-errorForeground);
440+ }
441+ .fm-optional {
442+ background: color-mix(in srgb, var(--vscode-descriptionForeground) 14%, transparent);
443+ color: var(--vscode-descriptionForeground);
444+ }
445+ .fm-property-desc {
446+ color: var(--vscode-descriptionForeground);
447+ margin-bottom: 6px;
448+ }
449+ .fm-property-value {
450+ background: var(--vscode-textCodeBlock-background);
451+ border-radius: 4px;
452+ font-family: 'Cascadia Code', 'Fira Code', monospace;
453+ font-size: 12px;
454+ padding: 5px 7px;
455+ white-space: pre-wrap;
456+ word-break: break-word;
457+ }
458+ .fm-empty {
459+ color: var(--vscode-descriptionForeground);
460+ font-style: italic;
461+ }
462+ .fm-raw summary {
463+ cursor: pointer;
464+ color: var(--vscode-textLink-foreground);
465+ margin-bottom: 8px;
466+ }
336467</style>
337468</head>
338469<body>
@@ -344,18 +475,23 @@ export class PreviewPanel implements Disposable {
344475 <div class="view-toggle">
345476 <button id="btn-rendered" class="active">Rendered</button>
346477 <button id="btn-raw">Raw JSON</button>
478+ <button id="btn-frontmatter">Frontmatter</button>
347479 </div>
348480 <div id="view-rendered">${ messagesHtml } </div>
349481 <div id="view-raw" class="hidden"><pre class="raw-json">${ rawJson } </pre></div>
482+ <div id="view-frontmatter" class="hidden">${ frontmatterHtml } </div>
350483 <script nonce="${ nonce } ">
351484 function showView(view) {
352485 document.getElementById('view-rendered').className = view === 'rendered' ? '' : 'hidden';
486+ document.getElementById('view-frontmatter').className = view === 'frontmatter' ? '' : 'hidden';
353487 document.getElementById('view-raw').className = view === 'raw' ? '' : 'hidden';
354488 document.getElementById('btn-rendered').className = view === 'rendered' ? 'active' : '';
489+ document.getElementById('btn-frontmatter').className = view === 'frontmatter' ? 'active' : '';
355490 document.getElementById('btn-raw').className = view === 'raw' ? 'active' : '';
356491 }
357492 document.getElementById('btn-rendered').addEventListener('click', function() { showView('rendered'); });
358493 document.getElementById('btn-raw').addEventListener('click', function() { showView('raw'); });
494+ document.getElementById('btn-frontmatter').addEventListener('click', function() { showView('frontmatter'); });
359495 </script>
360496</body>
361497</html>` ;
@@ -403,3 +539,216 @@ function escapeHtml(s: string): string {
403539function truncate ( s : string , max : number ) : string {
404540 return s . length > max ? s . slice ( 0 , max ) + '…' : s ;
405541}
542+
543+ function buildFrontmatterHtml ( agent : Prompty ) : string {
544+ const model = agent . model ;
545+ const connection = toRecord ( model ?. connection ) ;
546+ const template = toRecord ( agent . template ) ;
547+ const format = toRecord ( template . format ) ;
548+ const parser = toRecord ( template . parser ) ;
549+ const metadata = agent . metadata ?? { } ;
550+
551+ const cards = [
552+ frontmatterCard ( 'Name' , agent . displayName || agent . name || 'unnamed' ) ,
553+ frontmatterCard ( 'Model' , [ model ?. provider , model ?. id ] . filter ( Boolean ) . join ( ' - ' ) || 'not configured' ) ,
554+ frontmatterCard ( 'API' , model ?. apiType ?? 'chat' ) ,
555+ frontmatterCard ( 'Template' , [ format . kind , parser . kind ] . filter ( Boolean ) . join ( ' -> ' ) || 'default' ) ,
556+ frontmatterCard ( 'Connection' , describeConnection ( connection ) ) ,
557+ ] ;
558+
559+ const sections = [
560+ agent . description ? frontmatterSection ( 'Description' , `<p class="fm-description">${ escapeHtml ( agent . description ) } </p>` ) : '' ,
561+ propertySection ( 'Inputs' , agent . inputs ?? [ ] ) ,
562+ propertySection ( 'Outputs' , agent . outputs ?? [ ] ) ,
563+ keyValueSection ( 'Model options' , optionEntries ( model ?. options ) ) ,
564+ toolsSection ( agent . tools ?? [ ] ) ,
565+ metadataSection ( metadata ) ,
566+ rawFrontmatterSection ( agent ) ,
567+ ] . filter ( Boolean ) . join ( '\n' ) ;
568+
569+ return `<div class="fm-grid">${ cards . join ( '\n' ) } </div>${ sections } ` ;
570+ }
571+
572+ function frontmatterCard ( label : string , value : unknown ) : string {
573+ return `<div class="fm-card">
574+ <div class="fm-card-label">${ escapeHtml ( label ) } </div>
575+ <div class="fm-card-value">${ escapeHtml ( formatValue ( value , 120 ) ) } </div>
576+ </div>` ;
577+ }
578+
579+ function frontmatterSection ( title : string , body : string ) : string {
580+ return `<section class="fm-section">
581+ <h3>${ escapeHtml ( title ) } </h3>
582+ ${ body }
583+ </section>` ;
584+ }
585+
586+ function propertySection ( title : string , properties : {
587+ name ?: string ;
588+ kind ?: string ;
589+ required ?: boolean ;
590+ default ?: unknown ;
591+ example ?: unknown ;
592+ description ?: string ;
593+ enumValues ?: unknown [ ] ;
594+ } [ ] ) : string {
595+ if ( properties . length === 0 ) {
596+ return '' ;
597+ }
598+ const cards = properties . map ( ( prop ) => {
599+ const value = prop . default !== undefined ? prop . default : prop . example ;
600+ const enumHtml = prop . enumValues && prop . enumValues . length > 0
601+ ? `<div>${ prop . enumValues . map ( item => `<span class="fm-chip">${ escapeHtml ( formatValue ( item , 40 ) ) } </span>` ) . join ( '' ) } </div>`
602+ : '' ;
603+ const valueHtml = value !== undefined
604+ ? `<div class="fm-property-value">${ escapeHtml ( formatValue ( value , 300 ) ) } </div>`
605+ : '' ;
606+ return `<div class="fm-property">
607+ <div class="fm-property-head">
608+ <span class="fm-property-name">${ escapeHtml ( prop . name || '(unnamed)' ) } </span>
609+ <span class="fm-kind">${ escapeHtml ( prop . kind || 'unknown' ) } </span>
610+ <span class="${ prop . required ? 'fm-required' : 'fm-optional' } ">${ prop . required ? 'required' : 'optional' } </span>
611+ </div>
612+ ${ prop . description ? `<div class="fm-property-desc">${ escapeHtml ( prop . description ) } </div>` : '' }
613+ ${ valueHtml }
614+ ${ enumHtml }
615+ </div>` ;
616+ } ) . join ( '\n' ) ;
617+
618+ return frontmatterSection ( title , `<div class="fm-property-list">${ cards } </div>` ) ;
619+ }
620+
621+ function keyValueSection ( title : string , entries : [ string , unknown ] [ ] ) : string {
622+ if ( entries . length === 0 ) {
623+ return '' ;
624+ }
625+ const rows = entries . map ( ( [ key , value ] ) => `<tr>
626+ <td>${ escapeHtml ( key ) } </td>
627+ <td>${ escapeHtml ( formatValue ( value , 140 ) ) } </td>
628+ </tr>` ) . join ( '\n' ) ;
629+ return frontmatterSection ( title , `<table class="fm-table">
630+ <thead><tr><th>Setting</th><th>Value</th></tr></thead>
631+ <tbody>${ rows } </tbody>
632+ </table>` ) ;
633+ }
634+
635+ function toolsSection ( tools : { name ?: string ; kind ?: string ; description ?: string ; save ?: ( ) => Record < string , unknown > } [ ] ) : string {
636+ if ( tools . length === 0 ) {
637+ return '' ;
638+ }
639+ const rows = tools . map ( ( tool ) => {
640+ const saved = saveObject ( tool ) ;
641+ const parameters = toRecord ( saved . parameters ) ;
642+ const parameterNames = Object . keys ( toRecord ( parameters . properties ) ) ;
643+ const details = parameterNames . length > 0
644+ ? parameterNames . join ( ', ' )
645+ : Object . keys ( saved ) . filter ( k => ! [ 'name' , 'kind' , 'description' ] . includes ( k ) ) . join ( ', ' ) ;
646+ return `<tr>
647+ <td>${ escapeHtml ( tool . name || '(unnamed)' ) } </td>
648+ <td>${ escapeHtml ( tool . kind || 'unknown' ) } </td>
649+ <td>${ escapeHtml ( tool . description ?? '' ) } </td>
650+ <td>${ escapeHtml ( details || '' ) } </td>
651+ </tr>` ;
652+ } ) . join ( '\n' ) ;
653+ return frontmatterSection ( 'Tools' , `<table class="fm-table">
654+ <thead><tr><th>Name</th><th>Kind</th><th>Description</th><th>Details</th></tr></thead>
655+ <tbody>${ rows } </tbody>
656+ </table>` ) ;
657+ }
658+
659+ function metadataSection ( metadata : Record < string , unknown > ) : string {
660+ const entries = Object . entries ( metadata ) ;
661+ if ( entries . length === 0 ) {
662+ return '' ;
663+ }
664+ const chipKeys = new Set ( [ 'authors' , 'tags' ] ) ;
665+ const chips = entries
666+ . filter ( ( [ key , value ] ) => chipKeys . has ( key ) && Array . isArray ( value ) )
667+ . flatMap ( ( [ key , value ] ) => ( value as unknown [ ] ) . map ( item => `<span class="fm-chip">${ escapeHtml ( key ) } : ${ escapeHtml ( String ( item ) ) } </span>` ) )
668+ . join ( '' ) ;
669+ const rows = entries
670+ . filter ( ( [ key ] ) => ! chipKeys . has ( key ) )
671+ . map ( ( [ key , value ] ) => `<tr><td>${ escapeHtml ( key ) } </td><td>${ escapeHtml ( formatValue ( value , 140 ) ) } </td></tr>` )
672+ . join ( '\n' ) ;
673+ const table = rows ? `<table class="fm-table"><tbody>${ rows } </tbody></table>` : '' ;
674+ return frontmatterSection ( 'Metadata' , chips || table ? `${ chips } ${ table } ` : '<div class="fm-empty">No metadata</div>' ) ;
675+ }
676+
677+ function rawFrontmatterSection ( agent : Prompty ) : string {
678+ const saved = redactSecrets ( saveObject ( agent ) ) ;
679+ return frontmatterSection ( 'Raw frontmatter' , `<details class="fm-raw">
680+ <summary>Show normalized frontmatter JSON</summary>
681+ <pre class="raw-json">${ escapeHtml ( JSON . stringify ( saved , null , 2 ) ) } </pre>
682+ </details>` ) ;
683+ }
684+
685+ function optionEntries ( options : unknown ) : [ string , unknown ] [ ] {
686+ const saved = saveObject ( options ) ;
687+ const additionalProperties = toRecord ( saved . additionalProperties ) ;
688+ delete saved . additionalProperties ;
689+ return [
690+ ...Object . entries ( saved ) ,
691+ ...Object . entries ( additionalProperties ) . map ( ( [ key , value ] ) => [ `additionalProperties.${ key } ` , value ] as [ string , unknown ] ) ,
692+ ] ;
693+ }
694+
695+ function describeConnection ( connection : Record < string , unknown > ) : string {
696+ if ( Object . keys ( connection ) . length === 0 ) {
697+ return 'not configured' ;
698+ }
699+ const parts = [ connection . kind , connection . name , connection . endpoint , connection . target ]
700+ . filter ( ( value ) : value is string => typeof value === 'string' && value . length > 0 ) ;
701+ return parts . join ( ' - ' ) || 'configured' ;
702+ }
703+
704+ function formatValue ( value : unknown , max : number ) : string {
705+ if ( value === undefined || value === null ) {
706+ return '' ;
707+ }
708+ if ( typeof value === 'string' ) {
709+ return truncate ( value , max ) ;
710+ }
711+ if ( typeof value === 'number' || typeof value === 'boolean' ) {
712+ return String ( value ) ;
713+ }
714+ return truncate ( JSON . stringify ( redactSecrets ( value ) ) , max ) ;
715+ }
716+
717+ function saveObject ( value : unknown ) : Record < string , unknown > {
718+ if ( ! value || typeof value !== 'object' ) {
719+ return { } ;
720+ }
721+ const maybeSave = ( value as { save ?: ( ) => Record < string , unknown > } ) . save ;
722+ if ( typeof maybeSave === 'function' ) {
723+ return maybeSave . call ( value ) ;
724+ }
725+ return { ...( value as Record < string , unknown > ) } ;
726+ }
727+
728+ function toRecord ( value : unknown ) : Record < string , unknown > {
729+ return saveObject ( value ) ;
730+ }
731+
732+ function redactSecrets ( value : unknown ) : unknown {
733+ if ( Array . isArray ( value ) ) {
734+ return value . map ( redactSecrets ) ;
735+ }
736+ if ( ! value || typeof value !== 'object' ) {
737+ return value ;
738+ }
739+ const result : Record < string , unknown > = { } ;
740+ for ( const [ key , item ] of Object . entries ( value as Record < string , unknown > ) ) {
741+ result [ key ] = isSensitiveKey ( key ) ? '[redacted]' : redactSecrets ( item ) ;
742+ }
743+ return result ;
744+ }
745+
746+ function isSensitiveKey ( key : string ) : boolean {
747+ const normalized = key . toLowerCase ( ) ;
748+ return normalized . includes ( 'apikey' )
749+ || normalized . includes ( 'api_key' )
750+ || normalized . includes ( 'secret' )
751+ || normalized . includes ( 'password' )
752+ || normalized . includes ( 'token' )
753+ || normalized . includes ( 'credential' ) ;
754+ }
0 commit comments