@@ -216,6 +216,8 @@ function setupSidebarTree() {
216216 expandScopePath ( selectedScopeId ) ;
217217 if ( isContextBudgetOpen ( ) ) {
218218 openContextBudget ( selectedScopeId ) ;
219+ } else if ( ! document . getElementById ( "mcpControlsPanel" ) . classList . contains ( "hidden" ) ) {
220+ openMcpControlsPanel ( ) ;
219221 }
220222 const cat = catRow . dataset . cat ;
221223 if ( activeFilters . size === 1 && activeFilters . has ( cat ) ) {
@@ -246,6 +248,8 @@ function setupSidebarTree() {
246248 expandScopePath ( selectedScopeId ) ;
247249 if ( isContextBudgetOpen ( ) ) {
248250 openContextBudget ( selectedScopeId ) ;
251+ } else if ( ! document . getElementById ( "mcpControlsPanel" ) . classList . contains ( "hidden" ) ) {
252+ openMcpControlsPanel ( ) ;
249253 } else if ( selectedItem && selectedItem . scopeId !== selectedScopeId ) {
250254 closeDetail ( ) ;
251255 }
@@ -418,6 +422,17 @@ function setupDetailPanel() {
418422 document . getElementById ( "costBreakdown" ) . classList . add ( "hidden" ) ;
419423 document . querySelector ( ".detail-body" ) . classList . remove ( "hidden" ) ;
420424 } ) ;
425+
426+ // Metadata collapsible toggle
427+ const metaToggle = document . getElementById ( "detailMetaToggle" ) ;
428+ const metaBody = document . getElementById ( "detailMetaBody" ) ;
429+ if ( metaToggle && metaBody ) {
430+ metaToggle . addEventListener ( "click" , ( ) => {
431+ metaBody . classList . toggle ( "hidden" ) ;
432+ metaToggle . classList . toggle ( "open" ) ;
433+ metaToggle . querySelector ( ".d-meta-arrow" ) . textContent = metaBody . classList . contains ( "hidden" ) ? "▸" : "▾" ;
434+ } ) ;
435+ }
421436}
422437
423438function formatTokenCount ( n ) {
@@ -1183,6 +1198,9 @@ function renderDetailPanel(resetPreview = false) {
11831198 // Why it applies (Effective Behavior section)
11841199 renderEffectiveBehavior ( selectedItem ) ;
11851200
1201+ // Frontmatter config (model, when_to_use, description, maxTurns etc.)
1202+ renderItemConfig ( selectedItem ) ;
1203+
11861204 // CC Actions — contextual prompt buttons
11871205 renderCcActions ( selectedItem ) ;
11881206
@@ -1281,6 +1299,142 @@ function renderEffectiveBehavior(item) {
12811299 text . textContent = why ;
12821300}
12831301
1302+ // ── Item Config (frontmatter fields for skills, agents, memories) ─────────────
1303+
1304+ const ITEM_CONFIG_FIELDS = {
1305+ skill : [
1306+ { key : "model" , label : "Model" , type : "select" , options : [ "" , "opus" , "sonnet" , "haiku" , "inherit" ] , labels : [ "(not set) — uses default model" , "opus" , "sonnet" , "haiku" , "inherit" ] , tooltip : "When set, Claude Code uses this model instead of your session default when running this skill. Official SKILL.md frontmatter field." } ,
1307+ { key : "when_to_use" , label : "When to use" , type : "text" , placeholder : "(not set) — describe when AI should auto-trigger this skill" , tooltip : "Claude Code may auto-invoke skills based on context. Setting this gives more specific guidance on when this skill should be triggered." } ,
1308+ ] ,
1309+ agent : [
1310+ { key : "model" , label : "Model" , type : "select" , options : [ "" , "opus" , "sonnet" , "haiku" , "inherit" ] , labels : [ "(not set) — uses default model" , "opus" , "sonnet" , "haiku" , "inherit" ] , tooltip : "When set, Claude Code uses this model instead of your session default when running this agent." } ,
1311+ { key : "maxTurns" , label : "Max turns" , type : "number" , placeholder : "(not set) — no limit" , tooltip : "Maximum number of agentic turns before the agent stops. Prevents runaway agents from burning tokens indefinitely." } ,
1312+ ] ,
1313+ memory : [
1314+ { key : "description" , label : "Description" , type : "text" , placeholder : "(not set) — one-line summary for Claude to decide relevance" , tooltip : "Claude Code uses this to decide whether this memory is relevant to the current conversation. A clear description improves recall accuracy." } ,
1315+ ] ,
1316+ } ;
1317+
1318+ let _itemConfigTimers = { } ;
1319+
1320+ function getItemFilePath ( item ) {
1321+ if ( item . category === "skill" ) return `${ item . path } /SKILL.md` ;
1322+ if ( item . category === "agent" ) return `${ item . path } ` ;
1323+ if ( item . category === "memory" ) return item . path ;
1324+ return null ;
1325+ }
1326+
1327+ async function renderItemConfig ( item ) {
1328+ const wrap = document . getElementById ( "detailItemConfig" ) ;
1329+ if ( ! wrap ) return ;
1330+
1331+ const fields = item ? ITEM_CONFIG_FIELDS [ item . category ] : null ;
1332+ if ( ! fields ) { wrap . classList . add ( "hidden" ) ; wrap . innerHTML = "" ; return ; }
1333+
1334+ const filePath = getItemFilePath ( item ) ;
1335+ if ( ! filePath ) { wrap . classList . add ( "hidden" ) ; return ; }
1336+
1337+ // Read file to get current frontmatter values
1338+ let fm = { } ;
1339+ try {
1340+ const res = await fetchJson ( `/api/file-content?path=${ encodeURIComponent ( filePath ) } ` ) ;
1341+ if ( ! res . ok ) { wrap . classList . add ( "hidden" ) ; return ; }
1342+ fm = parseFrontmatter ( res . content ) ;
1343+ } catch { wrap . classList . add ( "hidden" ) ; return ; }
1344+
1345+ // Build HTML
1346+ let html = "" ;
1347+ for ( const field of fields ) {
1348+ html += `<div class="d-item-config-row">` ;
1349+ html += `<span class="d-info-label" data-tooltip="${ esc ( field . tooltip ) } ">${ esc ( field . label ) } </span>` ;
1350+
1351+ if ( field . type === "select" ) {
1352+ html += `<select class="d-item-select" data-fm-key="${ esc ( field . key ) } " data-fm-path="${ esc ( filePath ) } ">` ;
1353+ for ( let i = 0 ; i < field . options . length ; i ++ ) {
1354+ const val = field . options [ i ] ;
1355+ const label = field . labels ?. [ i ] || val ;
1356+ const sel = ( fm [ field . key ] || "" ) === val ? " selected" : "" ;
1357+ html += `<option value="${ esc ( val ) } "${ sel } >${ esc ( label ) } </option>` ;
1358+ }
1359+ html += `</select>` ;
1360+ } else if ( field . type === "number" ) {
1361+ const val = fm [ field . key ] || "" ;
1362+ html += `<input type="number" class="d-item-input d-item-number" data-fm-key="${ esc ( field . key ) } " data-fm-path="${ esc ( filePath ) } " value="${ esc ( val ) } " placeholder="${ esc ( field . placeholder || "" ) } " min="1">` ;
1363+ } else {
1364+ const val = fm [ field . key ] || "" ;
1365+ html += `<input type="text" class="d-item-input" data-fm-key="${ esc ( field . key ) } " data-fm-path="${ esc ( filePath ) } " value="${ esc ( val ) } " placeholder="${ esc ( field . placeholder || "" ) } ">` ;
1366+ }
1367+ html += `</div>` ;
1368+ }
1369+
1370+ wrap . innerHTML = html ;
1371+ wrap . classList . remove ( "hidden" ) ;
1372+
1373+ // Bind events
1374+ wrap . querySelectorAll ( "select[data-fm-key]" ) . forEach ( sel => {
1375+ sel . addEventListener ( "change" , ( ) => saveFrontmatterField ( sel . dataset . fmPath , sel . dataset . fmKey , sel . value ) ) ;
1376+ } ) ;
1377+ wrap . querySelectorAll ( "input[data-fm-key]" ) . forEach ( inp => {
1378+ inp . addEventListener ( "input" , ( ) => {
1379+ const k = inp . dataset . fmKey ;
1380+ clearTimeout ( _itemConfigTimers [ k ] ) ;
1381+ _itemConfigTimers [ k ] = setTimeout ( ( ) => saveFrontmatterField ( inp . dataset . fmPath , inp . dataset . fmKey , inp . value ) , 600 ) ;
1382+ } ) ;
1383+ } ) ;
1384+ }
1385+
1386+ function parseFrontmatter ( content ) {
1387+ const match = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
1388+ if ( ! match ) return { } ;
1389+ const fm = { } ;
1390+ for ( const line of match [ 1 ] . split ( "\n" ) ) {
1391+ const colon = line . indexOf ( ":" ) ;
1392+ if ( colon < 1 ) continue ;
1393+ const key = line . slice ( 0 , colon ) . trim ( ) ;
1394+ const val = line . slice ( colon + 1 ) . trim ( ) ;
1395+ fm [ key ] = val ;
1396+ }
1397+ return fm ;
1398+ }
1399+
1400+ async function saveFrontmatterField ( filePath , key , value ) {
1401+ try {
1402+ const res = await fetchJson ( `/api/file-content?path=${ encodeURIComponent ( filePath ) } ` ) ;
1403+ if ( ! res . ok ) return ;
1404+
1405+ let content = res . content ;
1406+ const fmMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
1407+
1408+ if ( ! fmMatch ) {
1409+ const newFm = value ? `---\n${ key } : ${ value } \n---\n` : "" ;
1410+ content = newFm + content ;
1411+ } else {
1412+ let fmBody = fmMatch [ 1 ] ;
1413+ const lineRegex = new RegExp ( `^${ key } :.*$` , "m" ) ;
1414+
1415+ if ( value ) {
1416+ if ( lineRegex . test ( fmBody ) ) {
1417+ fmBody = fmBody . replace ( lineRegex , `${ key } : ${ value } ` ) ;
1418+ } else {
1419+ fmBody += `\n${ key } : ${ value } ` ;
1420+ }
1421+ } else {
1422+ fmBody = fmBody . replace ( lineRegex , "" ) . replace ( / \n { 2 , } / g, "\n" ) . trim ( ) ;
1423+ }
1424+ content = `---\n${ fmBody } \n---` + content . slice ( fmMatch [ 0 ] . length ) ;
1425+ }
1426+
1427+ await fetchJson ( "/api/save-frontmatter" , {
1428+ method : "POST" ,
1429+ headers : { "Content-Type" : "application/json" } ,
1430+ body : JSON . stringify ( { path : filePath , content } ) ,
1431+ } ) ;
1432+ toast ( `Updated ${ key } for ${ selectedItem ?. name || "item" } ` ) ;
1433+ } catch ( err ) {
1434+ toast ( `Failed to save: ${ err . message } ` , true ) ;
1435+ }
1436+ }
1437+
12841438function renderCcActions ( item ) {
12851439 const container = document . getElementById ( "detailCcActions" ) ;
12861440 const btnRow = document . getElementById ( "ccBtnRow" ) ;
0 commit comments