1"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[564],{86054:function(e,t,n){function a(e){var t=e.graphology,n=e.Sigma,a=e.container,r=e.theme,i=e.rendering||n.rendering||{},s=e.rootId||"root",o=e.rootLabel||s,l=e.rootSize||20,c=e.nodeSize||8,d=e.innerRootSize||Math.floor(l/2),p=e.innerNodeSize||Math.floor(c/2),u=e.onNodeClick||null,m=e.labelFont||"system-ui, sans-serif",g=e.particleBlend||.5,f=e.spawnRadius||3,h=e.zoomingRatio||1.15,v=new t.Graph({multi:!1,type:"directed"}),y=new Set,_=[],b=[],w=[],x=new Set,k={},S={},E=0,C=0,I=null;function A(e,t,n,a,i,u){if(!v.hasNode(e)){var m=v.hasNode(t)?v.getNodeAttribute(t,"x"):0,g=v.hasNode(t)?v.getNodeAttribute(t,"y"):0,h=e===s,x=2*Math.PI*(a||0)/(i||1)+.2*Math.random(),C=h?0:m+Math.cos(x)*f,I=h?0:g+Math.sin(x)*f;v.addNode(e,{label:h?o:e,x:C,y:I,size:h?l:c,color:r.accent}),y.add(e),k[e]=h?0:.1*Math.cos(x),S[e]=h?0:.1*Math.sin(x);var A="_inner_"+e;if(v.addNode(A,{label:"",x:C,y:I,size:h?d:p,color:r.innerColor}),b.push(A),t&&t!==e&&v.hasNode(t)){var B=u?e:t,R=u?t:e;v.addEdge(B,R,{size:1,color:r.edgeColor,label:n||""}),_.push({source:B,target:R});for(var j=0;j<2;j++){var T="_p_"+E++,D=(.13*_.length+.5*j)%1;v.addNode(T,{label:"",x:m,y:g,size:1,color:r.innerColor}),w.push({id:T,source:B,target:R,phase:D})}}}}var B=i.createNodeBorderProgram({borders:[{size:{value:.2,mode:"relative"},color:{attribute:"borderColor"}}]}),R=new n(v,a,{allowInvalidContainer:!0,zoomingRatio:h,defaultEdgeType:"arrow",defaultNodeType:"bordered",edgeProgramClasses:{arrow:i.EdgeArrowProgram},nodeProgramClasses:{bordered:B,circle:i.NodeCircleProgram},renderLabels:!0,renderEdgeLabels:!0,labelColor:{color:r.labelColor},edgeLabelColor:{color:r.edgeLabelColor},labelSize:10,edgeLabelSize:9,labelFont:m,labelRenderedSizeThreshold:4,zIndex:!0});return R.on("clickNode",function(e){var t=e.node;y.has(t)&&u&&u(t)}),R.on("enterNode",function(e){y.has(e.node)&&!x.has(e.node)&&(a.style.cursor="pointer")}),R.on("leaveNode",function(){a.style.cursor="default"}),R.setSetting("nodeReducer",function(e,t){if(e===s)return Object.assign({},t,{type:"bordered",size:l,color:r.accentDim,borderColor:r.accent,zIndex:2});if(e==="_inner_"+s){var n=Math.floor(200*(.5*Math.sin(.06*C)+.5)+30).toString(16);return n.length<2&&(n="0"+n),Object.assign({},t,{type:"circle",size:d,color:r.innerColor+n,label:"",zIndex:3})}if(0===e.indexOf("_inner_")){for(var a=e.slice(7),i=0,o=0;o<w.length;o++)if(w[o].target===a){var u=(.008*C+w[o].phase)%1,m=u>.85?(u-.85)/.15:0;m>i&&(i=m)}var f=Math.floor(220*i+20).toString(16);return f.length<2&&(f="0"+f),Object.assign({},t,{type:"circle",size:p,color:r.innerColor+f,label:"",zIndex:3})}for(var h=0;h<w.length;h++)if(w[h].id===e){var v=(.008*C+w[h].phase)%1,_=v<.1?v/.1:v>.85?(1-v)/.15:1,b=Math.floor(r.bgRGB[0]+g*(r.particleRGB[0]-r.bgRGB[0])*_),k=Math.floor(r.bgRGB[1]+g*(r.particleRGB[1]-r.bgRGB[1])*_),S=Math.floor(r.bgRGB[2]+g*(r.particleRGB[2]-r.bgRGB[2])*_);return Object.assign({},t,{type:"circle",size:1+.8*_,color:"#"+(b<16?"0":"")+b.toString(16)+(k<16?"0":"")+k.toString(16)+(S<16?"0":"")+S.toString(16),label:"",zIndex:1})}if(y.has(e)){var E=x.has(e)?r.accent:r.expandableColor;return Object.assign({},t,{type:"bordered",size:c,color:r.accentDim,borderColor:E,zIndex:2})}return t}),R.setSetting("edgeReducer",function(e,t){var n=Math.floor(50*(.3*Math.sin(.04*C)+.5)+30).toString(16);return n.length<2&&(n="0"+n),Object.assign({},t,{color:r.edgeColor+n,size:1})}),I=setInterval(function(){C++,function(){for(var e=Array.from(y),t=0;t<e.length;t++)for(var n=e[t],a=v.getNodeAttribute(n,"x"),r=v.getNodeAttribute(n,"y"),i=t+1;i<e.length;i++){var s=e[i],o=v.getNodeAttribute(s,"x"),l=v.getNodeAttribute(s,"y"),c=a-o,d=r-l,p=Math.sqrt(c*c+d*d)+.01,u=3/(p*p),m=c/p*u*.4,g=d/p*u*.4;k[n]+=m,S[n]+=g,k[s]-=m,S[s]-=g}for(var f=0;f<_.length;f++){var h=_[f],b=v.getNodeAttribute(h.source,"x"),w=v.getNodeAttribute(h.source,"y"),x=v.getNodeAttribute(h.target,"x"),E=v.getNodeAttribute(h.target,"y"),C=x-b,I=E-w,A=Math.sqrt(C*C+I*I)+.01,B=.024*A,R=C/A*B,j=I/A*B;k[h.source]+=R,S[h.source]+=j,k[h.target]-=R,S[h.target]-=j}for(var T=0;T<e.length;T++){var D=e[T],q=v.getNodeAttribute(D,"x"),M=v.getNodeAttribute(D,"y");k[D]-=.008*q,S[D]-=.008*M,k[D]*=.82,S[D]*=.82,v.setNodeAttribute(D,"x",q
1+k[D]),v.setNodeAttribute(D,"y",M+S[D])}}();for(var e=0;e<b.length;e++){var t=b[e],n=t.slice(7);v.hasNode(n)&&(v.setNodeAttribute(t,"x",v.getNodeAttribute(n,"x")),v.setNodeAttribute(t,"y",v.getNodeAttribute(n,"y")))}for(var a=0;a<w.length;a++){var r=w[a];if(v.hasNode(r.source)&&v.hasNode(r.target)){var i=v.getNodeAttribute(r.source,"x"),s=v.getNodeAttribute(r.source,"y"),o=v.getNodeAttribute(r.target,"x"),l=v.getNodeAttribute(r.target,"y"),c=(.008*C+r.phase)%1;v.setNodeAttribute(r.id,"x",i+(o-i)*c),v.setNodeAttribute(r.id,"y",s+(l-s)*c)}}R.refresh()},50),{renderer:R,graph:v,expandNode:function(e,t){if(!x.has(e)){v.hasNode(e)||A(e,e),x.add(e);for(var n=0;n<t.length;n++){var a=t[n];A(a.name,e,a.kind,n,t.length,a.reverse)}}},addNode:A,markExpanded:function(e){x.add(e)},isExpanded:function(e){return x.has(e)},destroy:function(){if(I&&(clearInterval(I),I=null),R)try{R.kill()}catch(e){}}}}function r(){return"var createStarGraph = ".concat(a.toString(),";")}n.d(t,{r:function(){return a},t:function(){return r}})},82564:function(e,t,n){n.r(t),n.d(t,{createAppLauncherModule:function(){return G},ensureAppsLoaded:function(){return H},getActiveSandbox:function(){return O},isAppInputRequested:function(){return U},resetAppsLoadCache:function(){return P}});var a=n(96900),r=n(83104);function i(e){return e&&e.app_id&&(e.id=e.app_id),e}async function s(e,t){let n=(await (0,a.getBackend)().data.query(r.queries.insertApp({app_id:e.id,name:e.name,version:e.version,description:e.description,author:e.author||null,icon:e.icon||null,source:e.source,categories:e.categories||[],tags:e.tags||[],embedding:t,modules:e.modules,interaction_mode:e.interaction_mode||"agent_driven",html_templates:e.html_templates||null,display_mode:e.display_mode||"panel",state_schema:e.state_schema||null,permissions:e.permissions,requested_functions:e.requested_functions||[],voice_hooks:e.voice_hooks||null,on_activate:e.on_activate||null,on_deactivate:e.on_deactivate||null,external_scripts:e.external_scripts||null,migrations:e.migrations||null,min_tier:e.min_tier||"free",version_history:e.version_history||[{version:e.version,published_at:new Date().toISOString()}],forked_from:e.forked_from||null,_content_hash:e._content_hash||null,published_at:e.published_at||null}))).rows;return n[0]?i(n[0]):e}async function o(e){let t=(await (0,a.getBackend)().data.query(r.queries.getAppByAppId(e))).rows;return t.length>0?i(t[0]):null}async function l(e,t,n){let i=r.queries.updateApp({app_id:e,patch:t,embedding:n});i&&await (0,a.getBackend)().data.query(i)}async function c(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:10;return(await (0,a.getBackend)().data.query(r.queries.searchApps({embedding:e,limit:t}))).rows.map(i)}async function d(e){return(await (0,a.getBackend)().data.query(r.queries.insertInstall({app_id:e.app_id,installed_version:e.installed_version,granted_permissions:e.granted_permissions,app_state:e.app_state||{},config:e.config||{},last_activated_at:e.last_activated_at||null,activation_count:e.activation_count||0}))).rows[0]||e}async function p(e){let t=(await (0,a.getBackend)().data.query(r.queries.getInstallByAppId(e))).rows;return t.length>0?t[0]:null}async function u(e,t){let n=r.queries.updateInstall({app_id:e,patch:t});n&&await (0,a.getBackend)().data.query(n)}async function m(e){await (0,a.getBackend)().data.query(r.queries.deleteInstallByAppId(e))}async function g(){return(await (0,a.getBackend)().data.query(r.queries.listInstalls())).rows}async function f(e){await (0,a.getBackend)().data.query(r.queries.incrementAppInstallCount(e))}let h={builtin:["display","workspace:read","workspace:write","data:read","voice:tts","voice:mic","search:web","data:write","functions:dynamic","process:spawn","system:appearance","llm:call","data:raw_query"],agent:["display","workspace:read","workspace:write"],community:["display"]},v={update_workspace:"workspace:write",get_workspace:"workspace:read",user_output:"voice:tts",get_user_input:"voice:mic",get_embedding:"data:read",call_llm:"llm:call",query:"data:raw_query"},y={get_metrics:"data:read",get_logs:"data:read",get_recent_logs:"data:read",search_logs:"data:read",get_log_categories:"data:read",retrieve_declarative_knowledge:"data:read",get_knowledge_graph_entities:"data:read",get_entity_detail:"data:read",search_knowledge:"data:read",list_todos:"data:read",get_todos_context:"data:read",save_metric:"data:write",add_log:"data:write",update_log:"data:write",store_declarative_knowledge:"data:write",delete_declarative_knowledge:"data:write",create_todo:"data:write",update_todo:"data:write",save_todo:"data:write",manage_todo:"data:write",query_db:"data:raw_query",store_to_db:"data:raw_query",create_dynamic_function:"functions:dynamic",load_dynamic_function:"functions:dynamic",list_dynamic_functions:"functions:dynamic",fork_process:"process:spawn",search_web:"search:web",set_design_pack:"system:appearance"};var _=n(41020);class b{async mount(e){if(this.mounted&&this.iframe&&!this.iframe.isConnected&&(this.messageHandler&&window.removeEventListener("message",this.messageHandler),this.themeHandler&&window.removeEventListener("smartchats:theme_change",this.themeHandler),this.iframe=null,this.mounted=!1),this.mounted)return;this.iframe=document.createElement("iframe"),this.iframe.sandbox.add("allow-scripts"),this.iframe.sandbox.add("allow-modals"),this.iframe.style.cssText="width:100%;height:100%;border:none;",this.iframe.setAttribute("data-app-id",this.manifest.id),this.iframe.srcdoc=this.buildSrcdoc(),this.messageHandler=this.handleMessage.bind(this),window.addEventListener("message",this.messageHandler);let t=new Promise(e=>{let t=n=>{var a,r;(null===(a=n.data)||void 0===a?void 0:a.type)==="app_bridge_ready"&&n.source===(null===(r=this.iframe)||void 0===r?void 0:r.contentWindow)&&(window.removeEventListener("message",t),this.sendInit(),e())};window.addEventListener("message",t)});if(e)e.appendChild(this.iframe);else{let e=document.createElement("div");e.style.cssText="position:fixed;width:0;height:0;overflow:hidden;pointer-events:none;",e.appendChild(this.iframe),document.body.appendChild(e)}this.themeHandler=e=>{var t,n;let a=null===(t=e.detail)||void 0===t?void 0:t.tokens;a&&(null===(n=this.iframe)||void 0===n?void 0:n.contentWindow)&&this.iframe.contentWindow.postMessage({type:"theme_update",tokens:a},"*")},window.addEventListener("smartchats:theme_change",this.themeHandler),this.mounted=!0,await t}async snapshotState(){try{let e=await this.callFunction("__get_state",{});return this.lastState=e,e}catch(e){return this.lastState}}destroy(){for(let[,e]of(this.themeHandler&&(window.removeEventListener("smartchats:theme_change",this.themeHandler),this.themeHandler=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.pendingCalls))e.reject(Error("App sandbox destroyed"));if(this.pendingCalls.clear(),this.iframe){var e;null===(e=this.iframe.parentElement)||void 0===e||e.removeChild(this.iframe),this.iframe=null}this.mounted=!1,this.lastState=null}getIframe(){return this.iframe}async callFunction(e,t){var n;if(!(null===(n=this.iframe)||void 0===n?void 0:n.contentWindow))throw Error("App sandbox not mounted");let a=++this.callId;return new Promise((n,r)=>{this.pendingCalls.set(a,{resolve:n,reject:r}),setTimeout(()=>{this.pendingCalls.has(a)&&(this.pendingCalls.delete(a),r(Error("App function '".concat(e,"' timed out"))))},3e4),this.iframe.contentWindow.postMessage({type:"call_function",name:e,args:t||{},callId:a},"*")})}deliverUserInput(e){var t;(null===(t=this.iframe)||void 0===t?void 0:t.contentWindow)&&(this.iframe.contentWindow.postMessage({type:"user_input",text:e},"*"),this.inputRequested=!1)}syncWorkspace(e){var t;(null===(t=this.iframe)||void 0===t?void 0:t.contentWindow)&&this.iframe.contentWindow.postMessage({type:"workspace_sync",state:e},"*")}isInputRequested(){return this.inputRequested}handleMessage(e){if(!this.iframe||e.source!==this.iframe.contentWindow)return;let t=e.data;if(t&&t.type)switch(t.type){case"app_util_call":this.handleUtilCall(t.method,t.args,t.callId);break;case"app_function_result":{let e=this.pendingCalls.get(t.callId);e&&(e.resolve(t.result),this.pendingCalls.delete(t.callId));break}case"app_function_error":{let e=this.pendingCalls.get(t.callId);e&&(e.reject(Error(t.error||"App function error")),this.pendingCalls.delete(t.callId));break}case"app_log":this.hostHandlers.onLog("[app:".concat(this.manifest.id,"] ").concat(t.message));break;case"app_feedback":this.hostHandlers.onFeedback(t.feedbackType)}}async handleUtilCall(e,t,n){var a;if(!(null===(a=this.iframe)||void 0===a?void 0:a.contentWindow))return;if("get_user_input"===e){let t=v[e];if(t&&!this.grantedPermissions.includes(t)){this.respondError(n,"Permission denied: ".concat(e," requires ").concat(t));return}this.inputRequested=!0;return}let r=v[e.startsWith("smartchats."),e];if(e.startsWith("smartchats.")){let t=e.slice(11);if(!this.grantedFunctions.includes(t)){this.respondError(n,"Permission denied: function '".concat(t,"' not granted"));return}}else if(r&&!this.grantedPermissions.includes(r)){this.respondError(n,"Permission denied: ".concat(e," requires ").concat(r));return}try{let a=await this.hostHandlers.onUtilCall(e,t);this.respond(n,a)}catch(e){this.respondError(n,e.message||String(e))}}respond(e,t){var n;(null===(n=this.iframe)||void 0===n?void 0:n.contentWindow)&&this.iframe.contentWindow.postMessage({type:"util_result",callId:e,result:t},"*")}respondError(e,t){var n;(null===(n=this.iframe)||void 0===n?void 0:n.contentWindow)&&this.iframe.contentWindow.postMessage({type:"util_error",callId:e,error:t},"*")}
1sendInit(){var e;if(!(null===(e=this.iframe)||void 0===e?void 0:e.contentWindow))return;let t={};if(this.manifest.state_schema)for(let[e,n]of Object.entries(this.manifest.state_schema))t[e]=n.default;this.install.app_state&&Object.assign(t,this.install.app_state),this.lastState&&Object.assign(t,this.lastState),this.iframe.contentWindow.postMessage({type:"app_init",config:{manifest:{id:this.manifest.id,name:this.manifest.name,version:this.manifest.version,description:this.manifest.description,icon:this.manifest.icon,interaction_mode:this.manifest.interaction_mode||"agent_driven"},initialState:t,grantedUtilMethods:this.grantedUtilMethods,grantedFunctions:this.grantedFunctions,onActivate:this.manifest.on_activate||null}},"*")}resolveScriptUrl(e){let t=e.startsWith("/")?"".concat(window.location.origin).concat(e):e;try{return new URL(t),t}catch(e){return null}}buildCspMeta(){let e=this.manifest.external_scripts||[];if(0===e.length)return'<meta http-equiv="Content-Security-Policy" content="script-src \'unsafe-inline\';">';let t=e.map(e=>this.resolveScriptUrl(e)).filter(Boolean).join(" ");return'<meta http-equiv="Content-Security-Policy" content="script-src \'unsafe-inline\' '.concat(t,';">')}buildExternalScriptTags(){return(this.manifest.external_scripts||[]).map(e=>this.resolveScriptUrl(e)).filter(e=>!!e).map(e=>'<script src="'.concat(e,'"></script>')).join("\n")}buildSrcdoc(){var e;let t="";try{let e=getComputedStyle(document.documentElement),n={};for(let t of Array.from(document.documentElement.style))t.startsWith("--sc-")&&(n[t]=e.getPropertyValue(t).trim());Object.keys(n).length>0&&(t=(0,_.BQ)(n))}catch(e){}let n=this.manifest.modules.flatMap(e=>(e.functions||[]).map(e=>"SmartChats.registerFunction(".concat(JSON.stringify(e.name),", ").concat(e.code,");"))).join("\n"),a=(null===(e=this.manifest.html_templates)||void 0===e?void 0:e.main)||"",r=this.buildCspMeta(),i=this.buildExternalScriptTags();return"<!DOCTYPE html>\n<html><head>\n".concat(r,'\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<style>\n').concat(t,"\n * { margin: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; }\n body { font-family: var(--sc-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif);\n background: var(--sc-background, #0d1117); color: var(--sc-text, #e6edf3); }\n</style>\n").concat(i,"\n</head><body>\n\n<script>").concat("\n'use strict';\n\nvar SmartChats = {\n // ââ Internal State ââ\n _pendingCalls: new Map(),\n _callId: 0,\n _inputResolve: null,\n _ready: false,\n\n // ââ AppHandle ââ\n app: {\n dom: document,\n state: {},\n fns: {},\n manifest: {},\n el: function(sel) { return document.querySelector(sel); },\n // setState + onRender stubs â replaced with real implementations in _init\n setState: function(patch) {\n if (patch && typeof patch === 'object') {\n for (var k in patch) {\n if (Object.prototype.hasOwnProperty.call(patch, k)) {\n this.state[k] = patch[k];\n }\n }\n }\n },\n onRender: function(fn) { this._pendingRenderFn = fn; },\n _pendingRenderFn: null,\n },\n\n // ââ Function Registration ââ\n registerFunction: function(name, fn) {\n this.app.fns[name] = fn;\n },\n\n // ââ Util (populated by _initUtil based on granted permissions) ââ\n util: {},\n\n // ââ Tier 0 methods (always available) ââ\n log: function(msg) {\n window.parent.postMessage({ type: 'app_log', message: String(msg) }, '*');\n },\n\n feedback: function(feedbackType) {\n window.parent.postMessage({ type: 'app_feedback', feedbackType: feedbackType }, '*');\n },\n\n // ââ Bridge Call (sends request to host, returns promise) ââ\n _call: function(method, args) {\n var callId = ++this._callId;\n var self = this;\n return new Promise(function(resolve, reject) {\n self._pendingCalls.set(callId, { resolve: resolve, reject: reject });\n window.parent.postMessage({\n type: 'app_util_call',\n method: method,\n args: args !== undefined ? args : {},\n callId: callId\n }, '*');\n });\n },\n\n // ââ Initialization (called by host after iframe loads) ââ\n _initUtil: function(grantedMethods) {\n var self = this;\n for (var i = 0; i < grantedMethods.length;
1 i++) {\n (function(method) {\n self.util[method] = function(args) {\n return self._call(method, args);\n };\n })(grantedMethods[i]);\n }\n // log and feedback are always available directly on SmartChats\n // but also add them to util for consistency\n self.util.log = self.log.bind(self);\n self.util.feedback = self.feedback.bind(self);\n },\n\n _initSmartChatsFunctions: function(grantedFunctions) {\n var self = this;\n self.util.smartchats = {};\n for (var i = 0; i < grantedFunctions.length; i++) {\n (function(fnName) {\n self.util.smartchats[fnName] = function(params) {\n return self._call('smartchats.' + fnName, params);\n };\n })(grantedFunctions[i]);\n }\n },\n\n _init: function(config) {\n this.log('_init v=' + (config.manifest && config.manifest.version || '?') + ' onActivate=' + (config.onActivate || 'none'));\n var self = this;\n try {\n this.app.manifest = config.manifest || {};\n // Merge into existing state object (preserves references held by app scripts)\n var _initState = config.initialState || {};\n for (var _ik in _initState) {\n if (Object.prototype.hasOwnProperty.call(_initState, _ik)) {\n this.app.state[_ik] = _initState[_ik];\n }\n }\n this._initUtil(config.grantedUtilMethods || []);\n this._initSmartChatsFunctions(config.grantedFunctions || []);\n this._ready = true;\n this.log('_init: basics done');\n\n // ââ Reactive state: setState + onRender ââ\n var _renderFn = this.app._pendingRenderFn || null;\n var _changedKeys = new Set();\n var _renderScheduled = false;\n\n this.app.onRender = function(fn) { _renderFn = fn; };\n\n this.app.setState = function(patch) {\n if (!patch || typeof patch !== 'object') return;\n var keys = [];\n for (var k in patch) {\n if (Object.prototype.hasOwnProperty.call(patch, k)) {\n self.app.state[k] = patch[k];\n _changedKeys.add(k);\n keys.push(k);\n }\n }\n self.log('setState: keys=' + keys.join(',') + ' renderFn=' + !!_renderFn);\n if (!_renderScheduled && _renderFn) {\n _renderScheduled = true;\n Promise.resolve().then(function() {\n _renderScheduled = false;\n var changed = _changedKeys;\n _changedKeys = new Set();\n self.log('onRender firing: changed=' + Array.from(changed).join(','));\n try { _renderFn(self.app.state, changed); }\n catch (e) { self.log('onRender error: ' + (e.message || e)); }\n });\n }\n };\n this.log('_init: setState/onRender done');\n\n // Built-in state accessors (used by get_app_state / set_app_state)\n function safeSerialize(val) {\n try { return JSON.parse(JSON.stringify(val)); }\n catch (e) { return '[unserializable: ' + (e.message || e) + ']'; }\n }\n this.registerFunction('__get_state', function(args) {\n try {\n if (args && args.key) return safeSerialize(self.app.state[args.key]);\n return safeSerialize(self.app.state);\n } catch (e) {\n return { error: 'Failed to read state: ' + (e.message || e) };\n }\n });\n this.registerFunction('__set_state', function(args) {\n try {\n self.app.state[args.key] = args.value;\n return { ok: true };\n } catch (e) {\n return { error: 'Failed to write state: ' + (e.message || e) };\n }\n });\n this.log('_init: state accessors done, fns=' + Object.keys(this.app.fns).join(','));\n\n // Call on_activate hook\n if (config.onActivate) {\n var activateFn = this.app.fns[config.onActivate];\n if (!activateFn) {\n this.log('on_activate: \"' + config.onActivate + '\" not found');\n } else {\n this.log('on_activate: calling ' + config.onActivate);
1\n Promise.resolve().then(function() {\n return activateFn({}, self.app, self.util);\n }).then(function(result) {\n self.log('on_activate completed');\n }).catch(function(err) {\n self.log('on_activate error: ' + (err && err.message ? err.message : String(err)));\n });\n }\n }\n } catch (e) {\n this.log('_init CRASHED: ' + (e && e.message ? e.message : String(e)));\n }\n },\n};\n\n// ââ Base Message Handler ââ\nwindow.addEventListener('message', function(e) {\n var msg = e.data;\n if (!msg || !msg.type) return;\n\n switch (msg.type) {\n case 'app_init':\n SmartChats._init(msg.config);\n break;\n\n case 'call_function': {\n var fn = SmartChats.app.fns[msg.name];\n if (!fn) {\n window.parent.postMessage({\n type: 'app_function_error',\n callId: msg.callId,\n error: 'Function not found: ' + msg.name\n }, '*');\n break;\n }\n Promise.resolve().then(function() {\n return fn(msg.args || {}, SmartChats.app, SmartChats.util);\n }).then(function(result) {\n window.parent.postMessage({\n type: 'app_function_result',\n callId: msg.callId,\n result: result\n }, '*');\n }).catch(function(err) {\n window.parent.postMessage({\n type: 'app_function_error',\n callId: msg.callId,\n error: (err && err.message) ? err.message : String(err)\n }, '*');\n });\n break;\n }\n\n case 'util_result': {\n var pending = SmartChats._pendingCalls.get(msg.callId);\n if (pending) {\n pending.resolve(msg.result);\n SmartChats._pendingCalls.delete(msg.callId);\n }\n break;\n }\n\n case 'util_error': {\n var pendingErr = SmartChats._pendingCalls.get(msg.callId);\n if (pendingErr) {\n pendingErr.reject(new Error(msg.error || 'Unknown error'));\n SmartChats._pendingCalls.delete(msg.callId);\n }\n break;\n }\n\n case 'user_input': {\n if (SmartChats._inputResolve) {\n SmartChats._inputResolve(msg.text);\n SmartChats._inputResolve = null;\n }\n break;\n }\n\n case 'workspace_sync': {\n if (msg.state) {\n Object.assign(SmartChats.app.state, msg.state);\n }\n break;\n }\n\n case 'theme_update': {\n // Apply updated CSS vars to iframe :root\n if (msg.tokens) {\n var root = document.documentElement;\n for (var key in msg.tokens) {\n if (msg.tokens.hasOwnProperty(key)) {\n root.style.setProperty(key, msg.tokens[key]);\n }\n }\n }\n break;\n }\n }\n});\n\n// Special handling for get_user_input: resolves via 'user_input' message\n// Override the util method after init to use the dedicated channel\n(function() {\n var originalInit = SmartChats._initUtil;\n SmartChats._initUtil = function(grantedMethods) {\n originalInit.call(this, grantedMethods);\n // Override get_user_input to use the dedicated input channel\n if (this.util.get_user_input) {\n var self = this;\n this.util.get_user_input = function() {\n return new Promise(function(resolve) {\n self._inputResolve = resolve;\n window.parent.postMessage({\n type: 'app_util_call',\n method: 'get_user_input',\n args: {},\n callId: ++self._callId\n }, '*');\n });\n };\n }\n };\n})();\n\n// Signal that bridge is loaded\nwindow.parent.postMessage({ type: 'app_bridge_ready' }, '*');\n","</script>\n\n").concat(a,"\n\n<script>\n").concat(n,"\n</script>\n\n</body></html>")}constructor(e,t,n){var a,r;this.iframe=null,this.pendingCalls=new Map,this.callId=0,this.messageHandler=null,this.themeHandler=null,this.inputRequested=!1,this.mounted=!1,this.lastState=null,this.manifest=e,this.install=t,this.grantedPermissions=t.granted_permissions,this.grantedFunctions=(a=e.requested_functions||[],r=t.granted_permissions,a.filter(e=>{let t=y[e];return!t||r.includes(t)})),this.grantedUtilMethods=function(e){let t=["log","feedback"];
1for(let[n,a]of Object.entries(v))e.includes(a)&&t.push(n);return t}(t.granted_permissions),this.hostHandlers=n}}let w={id:"counter",name:"Counter",version:"1.0.0",description:"A simple counter app. Increment, decrement, or reset. Tests the app platform plumbing: iframe sandbox, bridge communication, state persistence, and agent function proxying.",icon:"\uD83D\uDD22",source:"builtin",categories:["utility","test"],tags:["counter","test","builtin"],interaction_mode:"agent_driven",display_mode:"panel",permissions:h.builtin,requested_functions:[],html_templates:{main:'\n<style>\n body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; gap: 16px; }\n .count { font-size: 72px; font-weight: 700; color: #58a6ff; }\n .label { font-size: 14px; color: #8b949e; text-transform: uppercase; letter-spacing: 2px; }\n .buttons { display: flex; gap: 12px; }\n .btn { padding: 12px 28px; border-radius: 8px; border: 1px solid #30363d;\n background: #21262d; color: #e6edf3; font-size: 18px; cursor: pointer;\n transition: background 0.15s; min-width: 60px; }\n .btn:hover { background: #30363d; }\n .btn:active { background: #3d444d; }\n .btn.primary { background: #238636; border-color: #2ea043; }\n .btn.primary:hover { background: #2ea043; }\n .btn.danger { background: #da3633; border-color: #f85149; }\n .btn.danger:hover { background: #f85149; }\n .status { font-size: 13px; color: #484f58; margin-top: 8px; }\n</style>\n\n<div class="label">Counter</div>\n<div class="count" id="count">0</div>\n<div class="buttons">\n <button class="btn danger" onclick="handleDecrement()">-</button>\n <button class="btn" onclick="handleReset()">Reset</button>\n <button class="btn primary" onclick="handleIncrement()">+</button>\n</div>\n<div class="status" id="status">Ready</div>\n\n<script>\n var countEl = document.getElementById(\'count\');\n var statusEl = document.getElementById(\'status\');\n\n function render() {\n countEl.textContent = SmartChats.app.state.count || 0;\n }\n\n function handleIncrement() {\n SmartChats.app.fns.increment({}, SmartChats.app, SmartChats.util);\n }\n\n function handleDecrement() {\n SmartChats.app.fns.decrement({}, SmartChats.app, SmartChats.util);\n }\n\n function handleReset() {\n SmartChats.app.fns.reset_counter({}, SmartChats.app, SmartChats.util);\n }\n\n render();\n</script>\n'},state_schema:{count:{type:"number",default:0,description:"The current count value"}},modules:[{id:"main",name:"Counter",position:60,system_msg:"A counter app is active. The user can tap +/- buttons or ask you to increment/decrement/reset. Use the counter_ prefixed functions.",functions:[{name:"increment",description:"Increment the counter by 1",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.state.count = (app.state.count || 0) + 1;\n app.dom.getElementById('count').textContent = app.state.count;\n app.dom.getElementById('status').textContent = 'Incremented to ' + app.state.count;\n await util.update_workspace({ count: app.state.count });\n return { count: app.state.count };\n}"},{name:"decrement",description:"Decrement the counter by 1",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.state.count = (app.state.count || 0) - 1;\n app.dom.getElementById('count').textContent = app.state.count;\n app.dom.getElementById('status').textContent = 'Decremented to ' + app.state.count;\n await util.update_workspace({ count: app.state.count });\n return { count: app.state.count };\n}"},{name:"reset_counter",description:"Reset the counter to 0",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.state.count = 0;\n app.dom.getElementById('count').textContent = '0';\n app.dom.getElementById('status').textContent = 'Reset';\n await util.update_workspace({ count: 0 });\n return { count: 0 };\n}"},{name:"set_count",description:"Set the counter to a specific value",parameters:{value:"number"},return_type:"object",code:"async function(fnArgs, app, util) {\n var val = Number(fnArgs.value) || 0;\n app.state.count = val;\n app.dom.getElementById('count').textContent = val;\n app.dom.getElementById('status').textContent = 'Set to ' + val;\n await util.update_workspace({ count: val });\n return { count: val };\n}"}]}],version_history:[{version:"1.0.0",published_at:new Date().toISOString()}]},x={id:"guided_breathing",name:"Guided Breathing",version:"1.4.0",description:"A visual breathing pacer using a square path animation. A dot travels clockwise through inhale, hold, exhale, hold phases. Tap or ask the agent to start, pause, resume, or stop.",icon:"\uD83C\uDF2Cï¸",source:"builtin",categories:["wellness","utility"],tags:["breathing","meditation","wellness","relaxation","builtin"],interaction_mode:"hybrid",display_mode:"panel",permissions:h.builtin,requested_functions:[],html_templates:{main:"\n<style>\n body { display: flex; flex-direction: column; align-items: center; justify-content: center;\n height: 100%; width: 100%; gap: 5vmin; user-select: none;\n padding: 4vmin; }\n .phase { font-size: clamp(18px, 5vmin, 28px); font-weight: 600; color: var(--sc-text);\n min-height: 1.5em; text-align: center; }\n .arena { position: relative; width: min(55vmin, 280px); height: min(55vmin, 280px);\n flex-shrink: 0; }\n .square { position: absolute; inset: 12%; border: 2px solid var(--sc-border); border-radius: 4px; }\n .dot { position: absolute; width: clamp(10px, 3.5vmin, 16px); height: clamp(10px, 3.5vmin, 16px);\n background: var(--sc-accent); border-radius: 50%; transform: translate(-50%, -50%);\n box-shadow: 0 0 12px color-mix(in srgb, var(--sc-accent) 40%, transparent); }\n .label { position: absolute; font-size: clamp(10px, 2.8vmin, 14px); color: var(--sc-text-muted);\n text-align: center; white-space: nowrap; transition: color 0.3s, opacity 0.3s; }\n .label.active { color: var(--sc-accent); font-weight: 600; }\n .label.top { top: 0; left: 50%; transform: translateX(-50%); }\n .label.right { right: 0; top: 50%; transform: translateX(50%) translateY(-50%); writing-mode: vertical-rl; }\n .label.bottom { bottom: 0; left: 50%; transform: translateX(-50%); }
1\n .label.left { left: 0; top: 50%; transform: translateX(-50%) translateY(-50%); writing-mode: vertical-rl; direction: rtl; }\n .timer { font-size: clamp(12px, 3vmin, 16px); color: var(--sc-text-muted); min-height: 1.2em;\n font-variant-numeric: tabular-nums; }\n .controls { display: flex; gap: 10px; }\n .btn { padding: 10px 24px; border-radius: var(--sc-radius-md, 10px); border: 1px solid var(--sc-border);\n background: var(--sc-surface); color: var(--sc-text); font-size: clamp(13px, 3vmin, 15px);\n cursor: pointer; transition: background 0.15s, transform 0.1s; }\n .btn:active { transform: scale(0.96); }\n .btn.primary { background: var(--sc-primary); color: white;\n border-color: color-mix(in srgb, var(--sc-primary) 80%, white 20%); }\n .btn.danger { background: var(--sc-danger); color: white;\n border-color: color-mix(in srgb, var(--sc-danger) 80%, white 20%); }\n .stats { font-size: clamp(11px, 2.5vmin, 13px); color: var(--sc-text-muted); opacity: 0.7; }\n</style>\n\n<div class=\"phase\" id=\"phase\">Ready</div>\n<div class=\"arena\">\n <div class=\"square\"></div>\n <div class=\"dot\" id=\"dot\"></div>\n <div class=\"label top\" id=\"lbl-inhale\">Inhale</div>\n <div class=\"label right\" id=\"lbl-hold1\">Hold</div>\n <div class=\"label bottom\" id=\"lbl-exhale\">Exhale</div>\n <div class=\"label left\" id=\"lbl-hold2\">Hold</div>\n</div>\n<div class=\"timer\" id=\"timer\"></div>\n<div class=\"controls\" id=\"controls\">\n <button class=\"btn primary\" id=\"startBtn\" onclick=\"handleStart()\">Start</button>\n</div>\n<div class=\"stats\" id=\"stats\"></div>\n\n<script>\n var PHASES = [\n { name: 'Inhale', label: 'lbl-inhale', instruction: 'Inhale through your nose' },\n { name: 'Hold', label: 'lbl-hold1', instruction: 'Hold' },\n { name: 'Exhale', label: 'lbl-exhale', instruction: 'Exhale through your mouth' },\n { name: 'Hold', label: 'lbl-hold2', instruction: 'Hold' },\n ];\n\n var timings = [4, 4, 4, 4];\n var running = false;\n var paused = false;\n var phaseIndex = 0;\n var phaseStart = 0;\n var pauseTime = 0;\n var cycles = 0;\n var rafId = null;\n\n var dot = document.getElementById('dot');\n var phaseEl = document.getElementById('phase');\n var timerEl = document.getElementById('timer');\n var controlsEl = document.getElementById('controls');\n var statsEl = document.getElementById('stats');\n\n function getCorners() {\n var a = document.querySelector('.arena');\n if (!a) return [[0,0],[1,0],[1,1],[0,1]];\n var w = a.offsetWidth, h = a.offsetHeight;\n var inset = 0.12;\n var l = w * inset, t = h * inset, r = w * (1 - inset), b = h * (1 - inset);\n return [[l, t], [r, t], [r, b], [l, b]];\n }\n\n function lerp(a, b, t) { return a + (b - a) * t; }\n\n function getDotPos(phase, progress) {\n var corners = getCorners();\n var c1 = corners[phase];\n var c2 = corners[(phase + 1) % 4];\n return [lerp(c1[0], c2[0], progress), lerp(c1[1], c2[1], progress)];\n }\n\n function updateLabels(activeIdx) {\n for (var i = 0; i < PHASES.length; i++) {\n var el = document.getElementById(PHASES[i].label);\n if (i === activeIdx) el.classList.add('active');\n else el.classList.remove('active');\n }\n }\n\n function renderControls() {\n if (!running) {\n controlsEl.innerHTML = '<button class=\"btn primary\" onclick=\"handleStart()\">Start</button>';\n } else if (paused) {\n controlsEl.innerHTML =\n '<button class=\"btn primary\" onclick=\"handleResume()\">Resume</button>' +\n '<button class=\"btn danger\" onclick=\"handleStop()\">Stop</button>';\n } else {\n controlsEl.innerHTML =\n '<button class=\"btn\" onclick=\"handlePause()\">Pause</button>' +\n '<button class=\"btn danger\" onclick=\"handleStop()\">Stop</button>';\n }\n }\n\n function tick() {\n if (!running || paused) return;\n var now = performance.now();\n var elapsed = (now - phaseStart) / 1000;\n var dur = timings[phaseIndex];\n var progress = Math.min(elapsed / dur, 1);\n\n var pos = getDotPos(phaseIndex, progress);\n dot.style.left = pos[0] + 'px';\n dot.style.top = pos[1] + 'px';\n\n var remaining = Math.max(0, dur - elapsed);\n timerEl.textContent = remaining.toFixed(1) + 's';\n\n if (progress >= 1) {\n phaseIndex = (phaseIndex + 1) % 4;\n if (phaseIndex === 0) cycles++;\n phaseStart = now;\n phaseEl.textContent = PHASES[phaseIndex].instruction;\n updateLabels(phaseIndex);\n statsEl.textContent = 'Cycles: ' + cycles;\n // Update app.state locally (no workspace push on every frame)\n SmartChats.app.state.cycles = cycles;\n SmartChats.app.state.phase = PHASES[phaseIndex].name;\n }\n\n rafId = requestAnimationFrame(tick);\n }\n\n function handleStart() {\n running = true; paused = false; phaseIndex = 0; cycles = 0;\n phaseStart = performance.now();\n phaseEl.textContent = PHASES[0].instruction;\n updateLabels(0);\n renderControls();\n SmartChats.app.state.running = true;\n SmartChats.app.state.paused = false;\n var pos = getDotPos(0, 0);\n dot.style.left = pos[0] + 'px';\n dot.style.top = pos[1] + 'px';\n rafId = requestAnimationFrame(tick);\n }\n\n function handlePause() {\n paused = true; pauseTime = performance.now();\n phaseEl.textContent = 'Paused';\n SmartChats.app.state.paused = true;\n renderControls();\n }\n\n function handleResume() {\n var pausedDuration = performance.now() - pauseTime;\n phaseStart += pausedDuration;\n paused = false;\n phaseEl.textContent = PHASES[phaseIndex].instruction;\n SmartChats.app.state.paused = false;\n renderControls();\n rafId = requestAnimationFrame(tick);\n }\n\n function handleStop() {\n running = false; paused = false;\n if (rafId) { cancelAnimationFrame(rafId); rafId = null; }\n phaseIndex = 0;\n phaseEl.textContent = 'Ready';\n timerEl.textContent = '';\n updateLabels(-1);\n SmartChats.app.state.running = false;\n SmartChats.app.state.paused = false;\n SmartChats.app.state.phase = 'Ready';\n var pos = getDotPos(0, 0);\n dot.style.left = pos[0] + 'px';\n dot.style.top = pos[1] + 'px';\n statsEl.textContent = cycles > 0 ? 'Completed ' + cycles + ' cycles' : '';\n renderControls();\n }
1\n\n // Initial dot position\n var initPos = getDotPos(0, 0);\n dot.style.left = initPos[0] + 'px';\n dot.style.top = initPos[1] + 'px';\n</script>\n"},state_schema:{running:{type:"boolean",default:!1,description:"Whether the breathing exercise is active"},paused:{type:"boolean",default:!1,description:"Whether the exercise is paused"},phase:{type:"string",default:"Ready",description:"Current phase: Inhale, Hold, Exhale, or Ready"},cycles:{type:"number",default:0,description:"Number of completed breathing cycles"},timings:{type:"array",default:[4,4,4,4],description:"Phase durations in seconds: [inhale, hold1, exhale, hold2]"}},modules:[{id:"main",name:"Guided Breathing",position:60,system_msg:"A guided breathing app is active. A square path animation shows a dot moving through inhaleâholdâexhaleâhold phases. The user can tap Start/Pause/Stop buttons directly, or ask you to control the exercise. Use the breathing_ prefixed functions.",functions:[{name:"start_breathing",description:"Start the breathing exercise",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n handleStart();\n await util.update_workspace({\n running: true, paused: false, phase: 'Inhale', cycles: 0\n });\n return { started: true, timings: app.state.timings || [4,4,4,4] };\n}"},{name:"pause_breathing",description:"Pause the breathing exercise",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n handlePause();\n await util.update_workspace({ paused: true });\n return { paused: true };\n}"},{name:"resume_breathing",description:"Resume the paused breathing exercise",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n handleResume();\n await util.update_workspace({ paused: false, running: true });\n return { resumed: true };\n}"},{name:"stop_breathing",description:"Stop the breathing exercise and reset",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var c = cycles || 0;\n handleStop();\n await util.update_workspace({ running: false, paused: false, phase: 'Ready', cycles: c });\n return { stopped: true, cycles: c };\n}"},{name:"set_breathing_timings",description:"Set custom phase durations in seconds. Order: inhale, hold, exhale, hold.",parameters:{inhale:"number",hold1:"number",exhale:"number",hold2:"number"},return_type:"object",code:"async function(fnArgs, app, util) {\n var t = [\n Number(fnArgs.inhale) || 4,\n Number(fnArgs.hold1) || 4,\n Number(fnArgs.exhale) || 4,\n Number(fnArgs.hold2) || 4\n ];\n if (typeof timings !== 'undefined') {\n timings[0] = t[0]; timings[1] = t[1]; timings[2] = t[2]; timings[3] = t[3];\n }\n app.state.timings = t;\n await util.update_workspace({ timings: t });\n return { timings: t };\n}"}]}],version_history:[{version:"1.0.0",published_at:new Date().toISOString()}]},k="\n function _logCall(app, name, t0, ok, err, util) {\n var duration_ms = performance.now() - t0;\n var callCount = (app.state.call_count || 0) + 1;\n var errorCount = (app.state.error_count || 0) + (!ok ? 1 : 0);\n var latencies = app.state.latencies || [];\n if (ok) {\n latencies = latencies.concat([duration_ms]);\n if (latencies.length > 100) latencies = latencies.slice(-100);\n }\n var entry = { name: name, ts: Date.now(), duration_ms: duration_ms, ok: ok };\n if (err) entry.error = String(err);\n var callLog = [entry].concat((app.state.call_log || []).slice(0, 49));\n\n app.setState({\n call_count: callCount,\n error_count: errorCount,\n latencies: latencies,\n call_log: callLog,\n });\n\n // Persist stats to workspace for cross-session survival\n if (util) {\n util.update_workspace({\n call_count: callCount,\n error_count: errorCount,\n latencies: latencies,\n });\n }\n }\n\n async function _runTest(name, fn) {\n var t0 = performance.now();\n try {\n await fn();\n return { name: name, ok: true, ms: performance.now() - t0 };\n } catch(e) {\n return { name: name, ok: false, ms: performance.now() - t0, error: String(e.message || e) };\n }\n }\n\n function _suiteResult(suite, tests, t0) {\n var passed = tests.filter(function(t){return t.ok;}).length;\n var failed = tests.length - passed;\n return { suite: suite, passed: passed, failed: failed, total: tests.length, duration_ms: performance.now() - t0, tests: tests };\n }\n\n function _updateHealth(app) {\n var sr = app.state.suite_results || {};\n var keys = Object.keys(sr);\n var health = 'idle';\n if (keys.length > 0) {\n var allPass = keys.every(function(k) { return sr[k] && sr[k].failed === 0; });\n var anyFail = keys.some(function(k) { return sr[k] && sr[k].failed > 0; });\n health = allPass ? 'healthy' : anyFail ? 'degraded' : 'idle';\n }\n app.setState({ health: health });\n }\n",S="async function(fnArgs, app, util) {\n ".concat(k,"\n ").concat("\n async function _suite_bridge(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('echo_roundtrip', async function() {\
1n var r = await app.fns.echo({ ping: 1, msg: 'hello' }, app, util);\n if (r.ping !== 1 || r.msg !== 'hello') throw new Error('Echo mismatch');\n }));\n\n tests.push(await _runTest('latency_10x', async function() {\n var times = [];\n for (var i = 0; i < 10; i++) {\n var lt0 = performance.now();\n await util.update_workspace({ __canary_lat_probe: i });\n times.push(performance.now() - lt0);\n }\n var avg = times.reduce(function(a,b){return a+b;},0) / times.length;\n if (avg > 5000) throw new Error('Avg latency ' + avg.toFixed(0) + 'ms exceeds 5s');\n }));\n\n tests.push(await _runTest('concurrent_3x', async function() {\n var results = await Promise.all([\n util.get_workspace(),\n util.get_workspace(),\n util.get_workspace(),\n ]);\n if (results.length !== 3) throw new Error('Expected 3 results');\n results.forEach(function(r, i) {\n if (typeof r !== 'object') throw new Error('Result ' + i + ' not an object');\n });\n }));\n\n tests.push(await _runTest('stress_20x', async function() {\n var times = [];\n for (var i = 0; i < 20; i++) {\n var st0 = performance.now();\n await app.fns.echo({ stress: i }, app, util);\n times.push(performance.now() - st0);\n }\n var avg = times.reduce(function(a,b){return a+b;},0) / times.length;\n var max = Math.max.apply(null, times);\n if (max > 10000) throw new Error('Slowest call ' + max.toFixed(0) + 'ms exceeds 10s');\n // Store throughput stats for dashboard\n app.state.__canary_stress = { calls: 20, avg_ms: Math.round(avg*10)/10, max_ms: Math.round(max*10)/10 };\n }));\n\n return _suiteResult('bridge', tests, t0);\n }\n","\n ").concat("\n async function _suite_state(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('app_state_rw', async function() {\n app.state.__canary_test_val = 42;\n if (app.state.__canary_test_val !== 42) throw new Error('State write/read failed');\n delete app.state.__canary_test_val;\n }));\n\n tests.push(await _runTest('workspace_rw', async function() {\n var marker = '__canary_ws_' + Date.now();\n await util.update_workspace({ __canary_test: marker });\n var ws = await util.get_workspace();\n var found = false;\n var keys = Object.keys(ws);\n for (var i = 0; i < keys.length; i++) {\n if (keys[i].indexOf('__canary_test') !== -1 && ws[keys[i]] === marker) { found = true; break; }\n }\n if (!found) throw new Error('Workspace write/read failed');\n }));\n\n tests.push(await _runTest('schema_defaults', async function() {\n if (typeof app.state.call_count !== 'number') throw new Error('call_count not initialized');\n if (!Array.isArray(app.state.call_log)) throw new Error('call_log not initialized');\n }));\n\n tests.push(await _runTest('workspace_sync_timing', async function() {\n // Write from app, then immediately read back â tests the sync race window\n var syncMarker = '__canary_sync_' + Date.now();\n await util.update_workspace({ __canary_sync_test: syncMarker });\n var ws = await util.get_workspace();\n // Find the value (may be prefixed with app id)\n var found = Object.keys(ws).some(function(k) { return ws[k] === syncMarker; });\n if (!found) throw new Error('Workspace sync: written value not readable immediately');\n }));\n\n tests.push(await _runTest('persistence_marker', async function() {\n // Write a known value that we can check after deactivate/reactivate\n app.state.__canary_persistence_check = 'persisted_' + Date.now();\n await util.update_workspace({ __canary_persistence_check: app.state.__canary_persistence_check });\n }));\n\n tests.push(await _runTest('setState_merge', async function() {\n // Verify setState merges and doesn't replace\n app.setState({ __canary_a: 1 });\n app.setState({ __canary_b: 2 });\n if (app.state.__canary_a !== 1) throw new Error('setState lost key a after second call');\n if (app.state.__canary_b !== 2) throw new Error('setState key b not set');\n delete app.state.__canary_a;\n delete app.state.__canary_b;\n }));\n\n tests.push(await _runTest('onRender_fires', async function() {\
1n // Verify onRender fires after setState with correct changed keys\n var renderFired = false;\n var receivedChanged = null;\n var prevRender = app._savedRender || null;\n\n app.onRender(function(state, changed) {\n renderFired = true;\n receivedChanged = changed;\n });\n\n app.setState({ __canary_render_test: 'hello' });\n\n // onRender is scheduled via microtask â wait for it\n await new Promise(function(r) { setTimeout(r, 10); });\n\n if (!renderFired) throw new Error('onRender did not fire after setState');\n if (!receivedChanged || !receivedChanged.has('__canary_render_test')) {\n throw new Error('onRender changed set missing __canary_render_test');\n }\n\n // Restore previous render function (canary's own renderAll)\n if (prevRender) app.onRender(prevRender);\n delete app.state.__canary_render_test;\n }));\n\n tests.push(await _runTest('onRender_batches', async function() {\n // Multiple setState calls in same microtask should trigger only one render\n var renderCount = 0;\n app.onRender(function() { renderCount++; });\n\n app.setState({ __canary_batch_a: 1 });\n app.setState({ __canary_batch_b: 2 });\n app.setState({ __canary_batch_c: 3 });\n\n // Wait for microtask to flush\n await new Promise(function(r) { setTimeout(r, 10); });\n\n if (renderCount !== 1) throw new Error('Expected 1 batched render, got ' + renderCount);\n\n delete app.state.__canary_batch_a;\n delete app.state.__canary_batch_b;\n delete app.state.__canary_batch_c;\n }));\n\n return _suiteResult('state', tests, t0);\n }\n","\n ").concat("\n async function _suite_permissions(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('util_methods_exist', async function() {\n if (typeof util.update_workspace !== 'function') throw new Error('update_workspace missing');\n if (typeof util.get_workspace !== 'function') throw new Error('get_workspace missing');\n if (typeof util.log !== 'function') throw new Error('log missing');\n if (typeof util.feedback !== 'function') throw new Error('feedback missing');\n }));\n\n tests.push(await _runTest('smartchats_fns_exist', async function() {\n var expected = ['save_log','get_recent_logs','search_logs','search_logs_semantic','get_log_categories',\n 'save_metric','get_metrics_context','retrieve_metrics',\n 'store_declarative_knowledge','retrieve_declarative_knowledge'];\n var missing = [];\n expected.forEach(function(fn) {\n if (!util.smartchats || typeof util.smartchats[fn] !== 'function') missing.push(fn);\n });\n if (missing.length > 0) throw new Error('Missing: ' + missing.join(', '));\n }));\n\n tests.push(await _runTest('function_count', async function() {\n var count = util.smartchats ? Object.keys(util.smartchats).length : 0;\n if (count < 10) throw new Error('Expected >= 10 smartchats functions, got ' + count);\n }));\n\n tests.push(await _runTest('voice_permissions', async function() {\n // Builtin apps get all permissions â verify voice util methods exist\n if (typeof util.user_output !== 'function') throw new Error('user_output missing (voice:tts)');\n if (typeof util.get_user_input !== 'function') throw new Error('get_user_input missing (voice:mic)');\n }));\n\n return _suiteResult('permissions', tests, t0);\n }\n","\n ").concat("\n async function _suite_data_metrics(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('write_metric', async function() {\n var r = await util.smartchats.save_metric({\n metric_name: '__canary_test',\n value: Date.now() % 1000,\n unit: 'test'\n });\n if (!r) throw new Error('save_metric returned falsy');\n }));\n\n // Brief delay for DB write propagation\n await new Promise(function(resolve) { setTimeout(resolve, 500); });\n\n tests.push(await _runTest('read_metric', async function() {\
1n var r = await util.smartchats.retrieve_metrics({\n metric_name: '__canary_test',\n recency: '1h'\n });\n if (!r || r.row_count < 1) throw new Error('Expected >= 1 row, got ' + (r ? r.row_count : 'null'));\n }));\n\n return _suiteResult('data_metrics', tests, t0);\n }\n","\n ").concat("\n async function _suite_data_logs(app, util) {\n var t0 = performance.now();\n var tests = [];\n var marker = '__canary_test_' + Date.now();\n\n tests.push(await _runTest('write_log', async function() {\n var r = await util.smartchats.save_log({ text: marker, category: '__canary' });\n if (!r || !r.saved) throw new Error('save_log failed: ' + JSON.stringify(r));\n }));\n\n // Brief delay for DB write propagation\n await new Promise(function(resolve) { setTimeout(resolve, 500); });\n\n tests.push(await _runTest('search_log', async function() {\n var r = await util.smartchats.search_logs({ text: '__canary_test_', category: '__canary' });\n if (!Array.isArray(r)) throw new Error('search_logs returned ' + typeof r + ' instead of array');\n if (r.length < 1) throw new Error('search_logs found 0 results');\n }));\n\n tests.push(await _runTest('search_semantic', async function() {\n var r = await util.smartchats.search_logs_semantic({ text: 'canary test log entry', limit: 5 });\n if (!Array.isArray(r)) throw new Error('search_logs_semantic returned ' + typeof r + ' instead of array');\n // Semantic search may return 0 results if no embeddings match â just verify the call succeeds\n }));\n\n return _suiteResult('data_logs', tests, t0);\n }\n","\n ").concat("\n async function _suite_data_kg(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('write_kg', async function() {\n var r = await util.smartchats.store_declarative_knowledge({\n triples: [['__canary_entity_a', '__canary_tests_with', '__canary_entity_b']]\n });\n if (!r) throw new Error('store_declarative_knowledge returned falsy');\n }));\n\n // Brief delay for DB write + embedding propagation\n await new Promise(function(resolve) { setTimeout(resolve, 500); });\n\n tests.push(await _runTest('read_kg', async function() {\n var r = await util.smartchats.retrieve_declarative_knowledge({\n query: '__canary_entity_a __canary_tests_with'\n });\n if (!r) throw new Error('retrieve_declarative_knowledge returned falsy');\n }));\n\n return _suiteResult('data_kg', tests, t0);\n }\n","\n ").concat("\n async function _suite_dom_theme(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('tokens_present', async function() {\n var bg = getComputedStyle(document.documentElement).getPropertyValue('--sc-background');\n if (!bg || bg.trim() === '') throw new Error('--sc-background is empty');\n }));\n\n tests.push(await _runTest('multi_tokens', async function() {\n var tokens = ['--sc-text', '--sc-surface', '--sc-border'];\n var missing = [];\n tokens.forEach(function(t) {\n var v = getComputedStyle(document.documentElement).getPropertyValue(t);\n if (!v || v.trim() === '') missing.push(t);\n });\n if (missing.length > 0) throw new Error('Missing: ' + missing.join(', '));\n }));\n\n tests.push(await _runTest('dom_manipulation', async function() {\n var el = document.createElement('div');\n el.id = '__canary_dom_test';\n el.textContent = 'test';\n document.body.appendChild(el);\n var found = document.getElementById('__canary_dom_test');\n if (!found) throw new Error('Element not found after append');\n if (found.textContent !== 'test') throw new Error('Content mismatch');\n document.body.removeChild(found);\n if (document.getElementById('__canary_dom_test')) throw new Error('Element not removed');\n }));\n\n return _suiteResult('dom_theme', tests, t0);\n }\n","\n ").concat("\n async function _suite_serialization(app, util) {\n var t0 = performance.now();\n var tests = [];\n\n tests.push(await _runTest('nested_object', async function() {\
1n var r = await app.fns.echo({ nested: {a:{b:{c:{d:{e:42}}}}} }, app, util);\n if (!r.nested || r.nested.a.b.c.d.e !== 42) throw new Error('Nested object corrupted');\n }));\n\n tests.push(await _runTest('type_handling', async function() {\n var r = await app.fns.echo({ n: null, b: true, s: 'str', arr: [1,2,3], num: 3.14 }, app, util);\n if (r.n !== null) throw new Error('null not preserved');\n if (r.b !== true) throw new Error('boolean not preserved');\n if (r.s !== 'str') throw new Error('string not preserved');\n if (!Array.isArray(r.arr) || r.arr.length !== 3) throw new Error('array not preserved');\n if (r.num !== 3.14) throw new Error('float not preserved');\n }));\n\n tests.push(await _runTest('depth_limit', async function() {\n var deep = {l1:{l2:{l3:{l4:{l5:{l6:{l7:{l8:{l9:{l10:'deep'}}}}}}}}}};\n var r = await app.fns.echo({ deep: deep }, app, util);\n // Just verify it doesn't crash â depth behavior is implementation-defined\n if (!r.deep) throw new Error('Deep object lost entirely');\n }));\n\n return _suiteResult('serialization', tests, t0);\n }\n","\n ").concat("\n function _handle_orchestration_report(fnArgs) {\n var chain = fnArgs.chain;\n var total_ms = fnArgs.total_ms;\n var tests = [];\n\n tests.push({ name: 'chain_length', ok: chain && chain.length === 3, ms: 0,\n error: (!chain || chain.length !== 3) ? 'Expected 3 items, got ' + (chain ? chain.length : 0) : undefined });\n\n if (chain && chain.length === 3) {\n tests.push({ name: 'seq_order', ok: chain[0].seq === 1 && chain[1].seq === 2 && chain[2].seq === 3, ms: 0,\n error: (chain[0].seq !== 1 || chain[1].seq !== 2 || chain[2].seq !== 3) ? 'Sequence mismatch' : undefined });\n\n tests.push({ name: 'chain_integrity', ok: chain[1].prev_seq === 1 && chain[2].prev_seq === 2, ms: 0,\n error: (chain[1].prev_seq !== 1 || chain[2].prev_seq !== 2) ? 'Return value chaining broken' : undefined });\n\n tests.push({ name: 'timing_reasonable', ok: total_ms < 15000, ms: total_ms,\n error: total_ms >= 15000 ? 'Orchestration took ' + total_ms + 'ms (>15s)' : undefined });\n }\n\n var passed = tests.filter(function(t){return t.ok;}).length;\n return { suite: 'orchestration', passed: passed, failed: tests.length - passed, total: tests.length, duration_ms: total_ms || 0, tests: tests };\n }\n","\n\n var t0 = performance.now();\n var suite = fnArgs.suite || 'all';\n\n try {\n var runners = {\n bridge: function() { return _suite_bridge(app, util); },\n state: function() { return _suite_state(app, util); },\n permissions: function() { return _suite_permissions(app, util); },\n data_metrics: function() { return _suite_data_metrics(app, util); },\n data_logs: function() { return _suite_data_logs(app, util); },\n data_kg: function() { return _suite_data_kg(app, util); },\n dom_theme: function() { return _suite_dom_theme(app, util); },\n serialization: function() { return _suite_serialization(app, util); },\n };\n\n if (suite === 'orchestration_report') {\n var orchResult = _handle_orchestration_report(fnArgs);\n var sr = Object.assign({}, app.state.suite_results || {});\n sr.orchestration = orchResult;\n app.setState({ suite_results: sr });\n _updateHealth(app);\n _logCall(app, 'run_suite:orchestration', t0, orchResult.failed === 0, null, util);\n await util.update_workspace({ suite_results: app.state.suite_results, health: app.state.health });\n return orchResult;\n }\n\n if (suite === 'all') {\n var allResults = {};\n var suiteIds = Object.keys(runners);\n for (var i = 0; i < suiteIds.length; i++) {\n var sid = suiteIds[i];\n var r = await runners[sid]();\n allResults[sid] = r;\n }\n app.setState({ suite_results: allResults });\n _updateHealth(app);\n _logCall(app, 'run_suite:all', t0, true, null, util);
1\n await util.update_workspace({ suite_results: app.state.suite_results, health: app.state.health });\n return { suite: 'all', results: allResults, health: app.state.health };\n }\n\n if (!runners[suite]) throw new Error('Unknown suite: ' + suite);\n\n var result = await runners[suite]();\n var sr2 = Object.assign({}, app.state.suite_results || {});\n sr2[suite] = result;\n app.setState({ suite_results: sr2 });\n _updateHealth(app);\n _logCall(app, 'run_suite:' + suite, t0, result.failed === 0, null, util);\n await util.update_workspace({ suite_results: app.state.suite_results, health: app.state.health });\n return result;\n } catch(e) {\n _logCall(app, 'run_suite:' + suite, t0, false, e, util);\n throw e;\n }\n}"),E={id:"canary",name:"Canary",version:"1.3.0",description:"Platform health dashboard. Validates every app platform layer: bridge, state, permissions, data access (metrics, logs, KG), DOM/theme, serialization, and agent orchestration. DevOps-style UI with real-time latency tracking and test runner.",icon:"\uD83D\uDC24",source:"builtin",categories:["devops","testing"],tags:["canary","testing","platform","builtin","reference"],interaction_mode:"hybrid",display_mode:"panel",permissions:h.builtin,requested_functions:["save_log","get_recent_logs","search_logs","search_logs_semantic","get_log_categories","save_metric","get_metrics_context","retrieve_metrics","store_declarative_knowledge","retrieve_declarative_knowledge"],html_templates:{main:"\n<style>\n :root {\n --cn-green: #3fb950;\n --cn-red: #f85149;\n --cn-yellow: #d29922;\n --cn-dim: #484f58;\n --cn-border: #30363d;\n --cn-surface: #161b22;\n --cn-bg: #0d1117;\n }\n * { margin: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body {\n display: flex; flex-direction: column;\n background: var(--sc-background, var(--cn-bg));\n color: var(--sc-text, #e6edf3);\n font-family: var(--sc-font-mono, 'SF Mono', 'Fira Code', monospace);\n font-size: 12px;\n }\n\n /* ââ Header ââ */\n #header {\n display: flex; align-items: center; gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--sc-border, var(--cn-border));\n background: var(--sc-surface, var(--cn-surface));\n flex-shrink: 0;\n }\n #header .title { font-size: 14px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; }\n #health-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--cn-dim);\n flex-shrink: 0;\n }\n #health-dot.healthy { background: var(--cn-green); box-shadow: 0 0 6px var(--cn-green); }\n #health-dot.degraded { background: var(--cn-yellow); box-shadow: 0 0 6px var(--cn-yellow); }\n #health-dot.failing { background: var(--cn-red); box-shadow: 0 0 6px var(--cn-red); }\n #health-label { color: var(--cn-dim); font-size: 11px; }\n #uptime { margin-left: auto; color: var(--cn-dim); font-size: 11px; }\n\n /* ââ Grid ââ */\n #grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-template-rows: auto 1fr;\n flex: 1; overflow: hidden;\n gap: 1px;\n background: var(--sc-border, var(--cn-border));\n }\n\n .panel {\n background: var(--sc-background, var(--cn-bg));\n padding: 10px 12px;\n overflow-y: auto;\n min-height: 0;\n }\n .panel-title {\n font-size: 10px; font-weight: 600; letter-spacing: 1.5px;\n text-transform: uppercase; color: var(--cn-dim);\n margin-bottom: 8px;\n }\n\n /* ââ Stats ââ */\n .stat-row { display: flex; justify-content: space-between; padding: 3px 0; }\n .stat-label { color: var(--cn-dim); }\n .stat-value { font-weight: 600; }\n\n /* ââ Call Log ââ */\n .log-entry {\n display: flex; gap: 8px; padding: 2px 0;\n font-size: 11px; border-bottom: 1px solid var(--sc-border, var(--cn-border));\n }\n .log-entry .fn-name { color: var(--sc-accent, #58a6ff); min-width: 80px; }\n .log-entry .timing { color: var(--cn-dim); min-width: 50px; text-align: right; }\n .log-entry .status-ok { color: var(--cn-green); }\n .log-entry .status-err { color: var(--cn-red); }\n\n /* ââ Suites ââ */\n .suite-row {\n display: flex; align-items: center; gap: 8px;\n padding: 6px 8px; cursor: pointer; border-radius: 4px;\n transition: background 0.12s;\n }\n .suite-row:hover { background: var(--sc-surface, var(--cn-surface)); }\n .suite-row:active { background: var(--sc-border, var(--cn-border)); }\n .suite-dot {\n width: 6px; height: 6px; border-radius: 50%;\n background: var(--cn-dim); flex-shrink: 0;\n }\n .suite-dot.pass { background: var(--cn-green); }\n .suite-dot.fail { background: var(--cn-red); }\n .suite-dot.running { background: var(--cn-yellow); animation: pulse 0.8s infinite; }\n @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }\n .suite-label { flex: 1; }\n .suite-result { color: var(--cn-dim); font-size: 11px; }\n\n #run-all-btn {\n display: block; width: 100%; margin-top: 10px;\n padding: 8px; border-radius: 4px;\n border: 1px solid var(--sc-border, var(--cn-border));\n background: var(--sc-surface, var(--cn-surface));\n color: var(--sc-text, #e6edf3);\n font-family: inherit; font-size: 11px; font-weight: 600;\n letter-spacing: 1px; text-transform: uppercase;\n cursor: pointer; transition: background 0.12s;\n }\n #run-all-btn:hover { background: var(--sc-border, var(--cn-border)); }\n #run-all-btn:active { background: #3d444d; }\n #run-all-btn:disabled { opacity: 0.4;
1 cursor: not-allowed; }\n\n /* ââ Results ââ */\n .test-row {\n display: flex; align-items: center; gap: 6px;\n padding: 3px 0; font-size: 11px;\n }\n .test-icon { width: 14px; text-align: center; flex-shrink: 0; }\n .test-name { flex: 1; }\n .test-ms { color: var(--cn-dim); min-width: 50px; text-align: right; }\n .test-err { color: var(--cn-red); font-size: 10px; padding-left: 20px; }\n\n #summary {\n margin-top: 10px; padding-top: 8px;\n border-top: 1px solid var(--sc-border, var(--cn-border));\n font-size: 11px; color: var(--cn-dim);\n }\n</style>\n\n<!-- Header -->\n<div id=\"header\">\n <span class=\"title\">Canary</span>\n <span id=\"health-dot\"></span>\n <span id=\"health-label\">idle</span>\n <span id=\"uptime\">00:00</span>\n</div>\n\n<!-- Grid -->\n<div id=\"grid\">\n <!-- Stats -->\n <div class=\"panel\" id=\"stats-panel\">\n <div class=\"panel-title\">Stats</div>\n <div class=\"stat-row\"><span class=\"stat-label\">Calls</span><span class=\"stat-value\" id=\"call-count\">0</span></div>\n <div class=\"stat-row\"><span class=\"stat-label\">Avg Latency</span><span class=\"stat-value\" id=\"avg-latency\">â</span></div>\n <div class=\"stat-row\"><span class=\"stat-label\">Errors</span><span class=\"stat-value\" id=\"error-count\">0</span></div>\n </div>\n\n <!-- Call Log -->\n <div class=\"panel\" id=\"call-log-panel\">\n <div class=\"panel-title\">Call Log</div>\n <div id=\"call-log\"></div>\n </div>\n\n <!-- Suites -->\n <div class=\"panel\" id=\"suites-panel\">\n <div class=\"panel-title\">Test Suites</div>\n <div id=\"suite-list\"></div>\n <button id=\"run-all-btn\" onclick=\"handleRunAll()\">Run All</button>\n </div>\n\n <!-- Results -->\n <div class=\"panel\" id=\"results-panel\">\n <div class=\"panel-title\">Results</div>\n <div id=\"results-list\"></div>\n <div id=\"summary\"></div>\n </div>\n</div>\n\n<script>\n /* ââ Suite definitions ââ */\n var SUITES = [\n { id: 'bridge', label: 'Bridge' },\n { id: 'state', label: 'State' },\n { id: 'permissions', label: 'Permissions' },\n { id: 'data_metrics', label: 'Data: Metrics' },\n { id: 'data_logs', label: 'Data: Logs' },\n { id: 'data_kg', label: 'Data: KG' },\n { id: 'dom_theme', label: 'DOM / Theme' },\n { id: 'serialization', label: 'Serialization' },\n ];\n\n /* ââ Render suite list ââ */\n var suiteListEl = document.getElementById('suite-list');\n SUITES.forEach(function(s) {\n var row = document.createElement('div');\n row.className = 'suite-row';\n row.id = 'suite-row-' + s.id;\n row.innerHTML = '<span class=\"suite-dot\" id=\"suite-dot-' + s.id + '\"></span>'\n + '<span class=\"suite-label\">' + s.label + '</span>'\n + '<span class=\"suite-result\" id=\"suite-res-' + s.id + '\"></span>';\n row.onclick = function() { handleRunSuite(s.id); };\n suiteListEl.appendChild(row);\n });\n\n /* ââ Uptime timer ââ */\n var startTime = Date.now();\n setInterval(function() {\n var s = Math.floor((Date.now() - startTime) / 1000);\n var m = Math.floor(s / 60);\n s = s % 60;\n document.getElementById('uptime').textContent =\n (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;\n }, 1000);
1\n\n /* ââ Render helpers ââ */\n function renderStats() {\n var st = SmartChats.app.state;\n document.getElementById('call-count').textContent = st.call_count || 0;\n document.getElementById('error-count').textContent = st.error_count || 0;\n var lats = st.latencies || [];\n if (lats.length > 0) {\n var avg = lats.reduce(function(a,b){return a+b;}, 0) / lats.length;\n document.getElementById('avg-latency').textContent = avg.toFixed(1) + 'ms';\n } else {\n document.getElementById('avg-latency').textContent = 'â';\n }\n }\n\n function renderHealth() {\n var h = SmartChats.app.state.health || 'idle';\n var dot = document.getElementById('health-dot');\n var label = document.getElementById('health-label');\n dot.className = 'health-dot';\n if (h === 'healthy') dot.className += ' healthy';\n else if (h === 'degraded') dot.className += ' degraded';\n else if (h === 'failing') dot.className += ' failing';\n label.textContent = h;\n }\n\n function renderCallLog() {\n var log = SmartChats.app.state.call_log || [];\n var el = document.getElementById('call-log');\n el.innerHTML = '';\n log.slice(0, 20).forEach(function(entry) {\n var row = document.createElement('div');\n row.className = 'log-entry';\n row.innerHTML = '<span class=\"fn-name\">' + entry.name + '</span>'\n + '<span class=\"timing\">' + (entry.duration_ms != null ? entry.duration_ms.toFixed(1) + 'ms' : 'â') + '</span>'\n + '<span class=\"' + (entry.ok ? 'status-ok' : 'status-err') + '\">' + (entry.ok ? 'ok' : 'err') + '</span>';\n el.appendChild(row);\n });\n }
1\n\n function renderSuiteStatus(suiteId, result) {\n var dot = document.getElementById('suite-dot-' + suiteId);\n var res = document.getElementById('suite-res-' + suiteId);\n if (!dot || !res) return;\n if (!result) { dot.className = 'suite-dot'; res.textContent = ''; return; }\n if (result.running) { dot.className = 'suite-dot running'; res.textContent = '...'; return; }\n var allPass = result.failed === 0;\n dot.className = 'suite-dot ' + (allPass ? 'pass' : 'fail');\n res.textContent = result.passed + '/' + result.total;\n }\n\n function renderResults(suiteResult) {\n var el = document.getElementById('results-list');\n if (!suiteResult || !suiteResult.tests) { el.innerHTML = ''; return; }\n el.innerHTML = '';\n suiteResult.tests.forEach(function(t) {\n var row = document.createElement('div');\n row.className = 'test-row';\n row.innerHTML = '<span class=\"test-icon\">' + (t.ok ? '<span style=\"color:var(--cn-green)\">+</span>' : '<span style=\"color:var(--cn-red)\">x</span>') + '</span>'\n + '<span class=\"test-name\">' + t.name + '</span>'\n + '<span class=\"test-ms\">' + (t.ms != null ? t.ms.toFixed(1) + 'ms' : '') + '</span>';\n el.appendChild(row);\n if (t.error) {\n var err = document.createElement('div');\n err.className = 'test-err';\n err.textContent = t.error;\n el.appendChild(err);\n }\n });\n }
1\n\n function renderSummary(allResults) {\n var el = document.getElementById('summary');\n if (!allResults) { el.textContent = ''; return; }\n var totalP = 0, totalF = 0, totalMs = 0;\n Object.keys(allResults).forEach(function(k) {\n var r = allResults[k];\n if (r && r.passed != null) { totalP += r.passed; totalF += r.failed; totalMs += (r.duration_ms || 0); }\n });\n el.textContent = 'Total: ' + totalP + '/' + (totalP + totalF) + ' ' + totalMs.toFixed(0) + 'ms';\n }\n\n function renderAll() {\n renderStats();\n renderHealth();\n renderCallLog();\n var sr = SmartChats.app.state.suite_results || {};\n SUITES.forEach(function(s) { renderSuiteStatus(s.id, sr[s.id]); });\n renderSummary(sr);\n }\n\n /* ââ Reactive rendering ââ */\n SmartChats.app.onRender(function(state, changed) {\n if (changed.has('call_count') || changed.has('error_count') || changed.has('latencies')) renderStats();\n if (changed.has('health')) renderHealth();\n if (changed.has('call_log')) renderCallLog();\n if (changed.has('suite_results')) {\n var sr = state.suite_results || {};\n SUITES.forEach(function(s) { renderSuiteStatus(s.id, sr[s.id]); });\n renderSummary(sr);\n }\n });\n\n /* ââ Click handlers ââ */\n function handleRunSuite(suiteId) {\n SmartChats.app.fns.run_suite({ suite: suiteId }, SmartChats.app, SmartChats.util);\n }\n\n function handleRunAll() {\n var btn = document.getElementById('run-all-btn');\n btn.disabled = true;\n btn.textContent = 'Running...';\n SmartChats.app.fns.run_suite({ suite: 'all' }, SmartChats.app, SmartChats.util).then(function() {\n btn.disabled = false;\n btn.textContent = 'Run All';\n }).catch(function() {\n btn.disabled = false;\n btn.textContent = 'Run All';\n });\n }\n\n renderAll();\n</script>\n"},state_schema:{health:{type:"string",default:"idle",description:"Overall health: idle | healthy | degraded | failing",persist:!1},suite_results:{type:"object",default:{},description:"Test results keyed by suite name",persist:!1},call_log:{type:"array",default:[],description:"Recent function call log entries",persist:!1},call_count:{type:"number",default:0,description:"Total function calls received",persist:!1},error_count:{type:"number",default:0,description:"Total errors encountered",persist:!1},latencies:{type:"array",default:[],description:"Recent latency measurements (ms)",persist:!1},activated_at:{type:"number",default:0,description:"Activation timestamp (ms)",persist:!1}},on_activate:"on_activate",modules:[{id:"main",name:"Canary",position:60,system_msg:'The Canary devops dashboard app is active. It validates every layer of the app platform. Available functions:\n- canary_echo: Returns args unchanged. Use for latency probes and orchestration testing.\n- canary_get_status: Returns dashboard snapshot (health, call counts, latencies, suite results).\n- canary_get_call_log: Returns recent function call log with timing.\n- canary_run_suite: Runs a test suite. Pass { suite: "bridge"|"state"|"permissions"|"data_metrics"|"data_logs"|"data_kg"|"dom_theme"|"serialization"|"all" }.\n\nFor orchestration testing, chain multiple echo calls and report results:\nvar t0 = Date.now();\nvar e1 = await canary_echo({ seq: 1, ts: Date.now() });\nvar e2 = await canary_echo({ seq: 2, ts: Date.now(), prev_seq: e1.seq });\nvar e3 = await canary_echo({ seq: 3, ts: Date.now(), prev_seq: e2.seq });\nawait canary_run_suite({ suite: \'orchestration_report\', chain: [e1, e2, e3], total_ms: Date.now() - t0 });',functions:[{name:"on_activate",description:"Initialize the canary dashboard on activation",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var now = Date.now();\n app.setState({ activated_at: now });\n await util.update_workspace({ activated_at: now });\n return { activated: true };\n}"},{name:"echo",description:"Returns args unchanged. Use for latency measurement and orchestration testing.",parameters:{any:"object"},return_type:"object",code:"async function(fnArgs, app, util) {\n ".concat(k,"\n var t0 = performance.now();\n try {\n var result = Object.assign({}, fnArgs, { _ts: Date.now() });\n _logCall(app, 'echo', t0, true, null, util);\n return result;\n } catch(e) {\n _logCall(app, 'echo', t0, false, e, util);\n throw e;\n }\n}")},{name:"get_status",description:"Returns current dashboard state: health, call counts, avg latency, suite results, uptime.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n ".concat(k,"\n var t0 = performance.now();\n try {\n var lats = app.state.latencies || [];\n var avg = lats.length > 0 ? lats.reduce(function(a,b){return a+b;},0) / lats.length : 0;\n var suites = {};\n var sr = app.state.suite_results || {};\n Object.keys(sr).forEach(function(k) {\n suites[k] = sr[k] && sr[k].failed === 0 ? 'pass' : 'fail';\n });
1\n var result = {\n health: app.state.health || 'idle',\n call_count: app.state.call_count || 0,\n error_count: app.state.error_count || 0,\n avg_latency_ms: Math.round(avg * 10) / 10,\n suites: suites,\n uptime_s: Math.floor((Date.now() - app.state.activated_at) / 1000),\n };\n _logCall(app, 'get_status', t0, true, null, util);\n return result;\n } catch(e) {\n _logCall(app, 'get_status', t0, false, e, util);\n throw e;\n }\n}")},{name:"get_call_log",description:"Returns recent function call log entries with timing data.",parameters:{limit:"number"},return_type:"array",code:"async function(fnArgs, app, util) {\n ".concat(k,"\n var t0 = performance.now();\n try {\n var limit = (fnArgs && fnArgs.limit) || 20;\n var result = (app.state.call_log || []).slice(0, limit);\n _logCall(app, 'get_call_log', t0, true, null, util);\n return result;\n } catch(e) {\n _logCall(app, 'get_call_log', t0, false, e, util);\n throw e;\n }\n}")},{name:"run_suite",description:"Run a test suite or all suites. Returns pass/fail results with timing. Suites: bridge, state, permissions, data_metrics, data_logs, data_kg, dom_theme, serialization, all. Special: orchestration_report (pass chain + total_ms from multi-call test).",parameters:{suite:"string"},return_type:"object",code:S},{name:"dom_check",description:"Check DOM state against app state. Returns whether UI elements match the data.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var suiteRows = app.dom.querySelectorAll('.suite-row').length;\n var logEntries = app.dom.querySelectorAll('.log-entry').length;\n var healthLabel = app.dom.getElementById('health-label').textContent;\n var callCount = app.dom.getElementById('call-count').textContent;\n var sr = app.state.suite_results || {};\n var suiteCount = Object.keys(sr).length;\n var suiteDots = app.dom.querySelectorAll('.suite-dot.pass, .suite-dot.fail').length;\n\n return {\n suite_rows: suiteRows,\n log_entries: logEntries,\n health_label: healthLabel,\n health_in_state: app.state.health || 'idle',\n health_match: healthLabel === (app.state.health || 'idle'),\n call_count_displayed: callCount,\n call_count_in_state: app.state.call_count || 0,\n suites_with_results: suiteCount,\n suite_dots_colored: suiteDots,\n suites_match: suiteDots === suiteCount,\n };\n}"}]}],version_history:[{version:"1.0.0",published_at:"2026-04-06T00:00:00.000Z"},{version:"1.1.0",published_at:"2026-04-06T00:00:00.000Z"},{version:"1.2.0",published_at:"2026-04-07T00:00:00.000Z"},{version:"1.3.0",published_at:new Date().toISOString()}]},C={id:"log_explorer",name:"Log Explorer",version:"1.1.0",description:"Browse, search, edit, and create journal log entries. Filter by category and date range, search by text, and edit entries inline.",icon:"\uD83D\uDCCB",source:"builtin",categories:["utility","data"],tags:["logs","journal","search","editor","builtin"],interaction_mode:"hybrid",display_mode:"panel",permissions:h.builtin,requested_functions:["get_recent_logs","search_logs","search_logs_semantic","get_log_categories","save_log","update_log"],html_templates:{main:"\n<style>\n * { margin: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body {\n display: flex; flex-direction: column;\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n font-family: var(--sc-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif);\n font-size: 13px;\n }\n\n /* ââ Shared form elements ââ */\n select, input[type=\"text\"], input[type=\"date\"], input[type=\"time\"] {\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: 6px;\n padding: 6px 10px;\n font-size: 12px; font-family: inherit; outline: none;\n }\n select:focus, input:focus { border-color: var(--sc-primary, #58a6ff); }\n .btn {\n background: var(--sc-surface, #161b22);\n color: var(--sc-text-muted, #8b949e);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: 6px;\n padding: 6px 14px;\n font-size: 12px; font-weight: 500; font-family: inherit;\n cursor: pointer; transition: all 0.15s;\n }\n .btn:hover { background: var(--sc-border, #30363d); color: var(--sc-text, #e6edf3); }\n .btn-primary {\n background: var(--sc-primary, #238636); color: #fff;\n border-color: transparent;\n }\n .btn-primary:hover { filter: brightness(1.15); }\n .btn:disabled { opacity: 0.4;
1 cursor: not-allowed; }\n\n /* ââ Filters bar ââ */\n #filters {\n display: flex; gap: 8px; align-items: center;\n padding: 10px 12px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0; flex-wrap: wrap;\n }\n #filters input[type=\"text\"] { flex: 1; min-width: 100px; }\n\n /* ââ Content area ââ */\n #content { display: flex; flex: 1; overflow: hidden; }\n\n /* ââ Log List ââ */\n #log-list-panel {\n width: 45%; min-width: 200px; max-width: 400px;\n overflow-y: auto; border-right: 1px solid var(--sc-border, #30363d);\n display: flex; flex-direction: column;\n }\n #log-count {\n padding: 6px 12px; font-size: 10px;\n color: var(--sc-text-muted, #8b949e);\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0;\n }\n #log-list { flex: 1; overflow-y: auto; }\n .log-item {\n padding: 10px 12px; cursor: pointer;\n border-bottom: 1px solid var(--sc-border, #30363d);\n transition: background 0.12s;\n }\n .log-item:hover { background: var(--sc-surface, #161b22); }\n .log-item.selected {\n background: var(--sc-surface-alt, #1c2128);\n border-left: 3px solid var(--sc-primary, #58a6ff);\n padding-left: 9px;\n }\n .log-meta {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 4px;\n }\n .log-category {\n font-size: 11px; font-weight: 600; text-transform: uppercase;\n letter-spacing: 0.5px; color: var(--sc-accent, #58a6ff);\n }\n .log-date { font-size: 10px; color: var(--sc-text-muted, #8b949e); }\n .log-preview {\n font-size: 12px; color: var(--sc-text-muted, #8b949e);\n line-height: 1.5;\n overflow: hidden; text-overflow: ellipsis;\n display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;\n }\n .log-item.expanded .log-preview {\n -webkit-line-clamp: unset; display: block;\n }\n #log-list-empty {\n padding: 40px 20px; text-align: center;\n color: var(--sc-text-muted, #8b949e); font-size: 12px;\n }\n\n /* ââ Editor ââ */\n #editor-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n #editor-empty {\n flex: 1; display: flex; align-items: center; justify-content: center;\n color: var(--sc-text-muted, #484f58); font-size: 13px;\n }\n #editor-form { display: none; flex: 1; flex-direction: column; }\n #editor-fields {\n display: grid; grid-template-columns: 1fr 1fr;\n gap: 8px; padding: 12px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n }\n .field-group { display: flex; flex-direction: column; gap: 3px; }\n .field-group.full { grid-column: 1 / -1; }\n .field-label {\n font-size: 10px; font-weight: 600; text-transform: uppercase;\n letter-spacing: 0.8px; color: var(--sc-text-muted, #8b949e);\n }\n #editor-content { flex: 1; display: flex; flex-direction: column; padding: 12px; }\n #editor-textarea {\n flex: 1; width: 100%; resize: none;\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: 8px;\n padding: 12px; font-size: 13px; font-family: inherit;\n line-height: 1.6; outline: none;\n }\n #editor-textarea:focus { border-color: var(--sc-primary, #58a6ff); }\n #editor-textarea::placeholder { color: var(--sc-text-muted, #484f58); }\n #editor-actions {\n display: flex; gap: 8px; align-items: center;\n padding: 10px 12px;\n border-top: 1px solid var(--sc-border, #30363d);\n }\n #editor-status {\n font-size: 11px; color: var(--sc-text-muted, #8b949e);\n margin-left: auto;\n }\n</style>\n\n<!-- Filters -->\n<div id=\"filters\">\n <select id=\"filter-category\" onchange=\"handleFilter()\">\n <option value=\"\">All categories</option>\n </select>\n <select id=\"filter-recency\" onchange=\"handleFilter()\">\n <option value=\"1d\">Today</option>\n <option value=\"3d\">3 days</option>\n <option value=\"1w\" selected>This week</option>\n <option value=\"2w\">2 weeks</option>\n <option value=\"4w\">This month</option>\n <option value=\"12w\">3 months</option>\n <option value=\"\">All time</option>\n </select>
1\n <input id=\"filter-search\" type=\"text\" placeholder=\"Search logs...\" onkeydown=\"if(event.key==='Enter')handleSearch()\" />\n <button class=\"btn\" onclick=\"handleSearch()\">Search</button>\n <button class=\"btn\" onclick=\"handleClear()\">Clear</button>\n <button class=\"btn\" onclick=\"handleNewLog()\">+ New</button>\n</div>\n\n<!-- Query Details -->\n<div id=\"query-details\" style=\"border-bottom:1px solid var(--sc-border,#30363d);font-size:11px;\">\n <div id=\"query-toggle\" onclick=\"toggleQueryDetails()\" style=\"padding:4px 12px;cursor:pointer;color:var(--sc-text-muted,#8b949e);display:flex;align-items:center;gap:6px;user-select:none;\">\n <span id=\"query-arrow\" style=\"font-size:9px;\">▶</span>\n <span>Query Details</span>\n </div>\n <div id=\"query-body\" style=\"display:none;padding:6px 12px 8px;background:var(--sc-surface,#161b22);\">\n <div style=\"display:flex;align-items:center;gap:8px;margin-bottom:6px;\">\n <span style=\"color:var(--sc-text-muted,#8b949e);\">Limit:</span>\n <input type=\"number\" id=\"query-limit\" value=\"500\" style=\"width:70px;\" onchange=\"handleFilter()\" />\n </div>\n <div id=\"query-text\" style=\"font-family:var(--sc-font-mono,monospace);color:var(--sc-text-muted,#8b949e);word-break:break-all;white-space:pre-wrap;\"></div>\n </div>\n</div>\n\n<!-- Content -->\n<div id=\"content\">\n <!-- Log List -->\n <div id=\"log-list-panel\">\n <div id=\"log-count\"></div>\n <div id=\"log-spinner\" style=\"display:none;padding:30px 20px;text-align:center;color:var(--sc-text-muted,#8b949e);font-size:12px\">Loading...</div>\n <div id=\"log-list\"></div>\n <div id=\"log-list-empty\" style=\"display:none\">No logs found</div>\n </div>\n\n <!-- Editor -->\n <div id=\"editor-panel\">\n <div id=\"editor-empty\">Select a log or create a new one</div>\n <div id=\"editor-form\">\n <div id=\"editor-fields\">\n <div class=\"field-group\">\n <span class=\"field-label\">Category</span>\n <select id=\"editor-category\"></select>\n </div>\n <div class=\"field-group\">\n <span class=\"field-label\">Date</span>\n <input type=\"date\" id=\"editor-date\" />\n </div>\n <div class=\"field-group\">\n <span class=\"field-label\">Time</span>\n <input type=\"time\" id=\"editor-time\" />\n </div>\n <div class=\"field-group\">\n <span class=\"field-label\" id=\"editor-id-label\"></span>\n </div>\n </div>\n <div id=\"editor-content\">\n <textarea id=\"editor-textarea\" placeholder=\"Write your log entry...\"></textarea>\n </div>\n <div id=\"editor-actions\">\n <button class=\"btn btn-primary\" id=\"btn-save\" onclick=\"handleSave()\">Save</button>\n <button class=\"btn\" onclick=\"handleCancel()\">Cancel</button>\n <span id=\"editor-status\"></span>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n var state = SmartChats.app.state;\n\n /* ââ Helpers ââ */\n function formatDate(dateStr) {\n if (!dateStr) return '';\n var d = new Date(dateStr);\n var now = new Date();\n var diff = now.getTime() - d.getTime();\n if (diff < 60000) return 'just now';\n if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';\n if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';\n if (diff < 604800000) return Math.floor(diff/86400000) + 'd ago';\n return d.toLocaleDateString();\n }\n\n function parseLogDate(dateStr) {\n // Accepts either a YYYY-MM-DD (local_date) or full ISO datetime (ts).\n // Strip a trailing Z and parse â Date.parse handles both shapes.\n if (!dateStr) return new Date();\n var s = String(dateStr).replace('Z', '');\n return new Date(s);\n }\n\n function toDateValue(d) {\n // YYYY-MM-DD for input[type=date]\n var y = d.getFullYear();\n var m = String(d.getMonth() + 1).padStart(2, '0');\n var day = String(d.getDate()).padStart(2, '0');\n return y + '-' + m + '-' + day;\n }\n\n function toTimeValue(d) {\n // HH:MM for input[type=time]\n var h = String(d.getHours()).padStart(2, '0');\n var m = String(d.getMinutes()).padStart(2, '0');\n return h + ':' + m;\n }\n\n /* ââ Render log list ââ */\n function renderLogList() {\n var logs = state.logs || [];\n var loading = state.loading;\n var listEl = document.getElementById('log-list');\n var emptyEl = document.getElementById('log-list-empty');\n var spinnerEl = document.getElementById('log-spinner');\n var countEl = document.getElementById('log-count');\n\n // Spinner\n spinnerEl.style.display = loading ? '' : 'none';\n if (loading) { listEl.innerHTML = ''; emptyEl.style.display = 'none'; countEl.textContent = ''; return; }\n\n listEl.innerHTML = '';\n var searchQuery = document.getElementById('filter-search').value.trim();\n countEl.textContent = logs.length + ' log' + (logs.length !== 1 ? 's' : '') + (searchQuery ? ' matching \"' + searchQuery + '\"' : '');\n\n if (logs.length === 0) { emptyEl.style.display = ''; return; }\n emptyEl.style.display = 'none';
1\n\n logs.forEach(function(log) {\n var item = document.createElement('div');\n var isSelected = state.selected_log_id === log.id;\n item.className = 'log-item' + (isSelected ? ' selected' : '');\n item.innerHTML = '<div class=\"log-meta\">'\n + '<span class=\"log-category\">' + (log.category || 'general') + '</span>'\n + '<span class=\"log-date\">' + formatDate(log.ts || log.created_at) + '</span>'\n + '</div>'\n + '<div class=\"log-preview\">' + (log.content || '').replace(/</g, '<') + '</div>';\n item.onclick = function(e) {\n if (e.detail === 2) {\n // Double-click: toggle expand preview\n item.classList.toggle('expanded');\n } else {\n selectLog(log);\n }\n };\n listEl.appendChild(item);\n });\n }\n\n /* ââ Render category dropdowns ââ */\n function renderCategories() {\n var cats = state.categories || [];\n var filterSel = document.getElementById('filter-category');\n var editorSel = document.getElementById('editor-category');\n var currentFilter = filterSel.value;\n var currentEditor = editorSel.value;\n\n filterSel.innerHTML = '<option value=\"\">All categories</option>';\n cats.forEach(function(c) {\n var opt = document.createElement('option');\n opt.value = c.category;\n opt.textContent = c.category + ' (' + c.count + ')';\n filterSel.appendChild(opt);\n });\n filterSel.value = currentFilter;\n\n editorSel.innerHTML = '';\n var allCats = cats.map(function(c) { return c.category; });\n if (allCats.indexOf('general') === -1) allCats.unshift('general');\n allCats.forEach(function(c) {\n var opt = document.createElement('option');\n opt.value = c;\n opt.textContent = c;\n editorSel.appendChild(opt);\n });\n editorSel.value = currentEditor || 'general';\n }\n\n /* ââ Select a log for editing ââ */\n function selectLog(log) {\n var d = parseLogDate(log.ts || log.created_at);\n SmartChats.app.setState({\n selected_log_id: log.id,\n _editing: {\n id: log.id,\n content: log.content || '',\n category: log.category || 'general',\n date: toDateValue(d),\n time: toTimeValue(d),\n is_new: false,\n }\n });\n }\n\n /* ââ Show/hide editor ââ */\n function showEditor() {\n var e = state._editing;\n if (!e) return;\n document.getElementById('editor-empty').style.display = 'none';\n var form = document.getElementById('editor-form');\n form.style.display = 'flex';\n document.getElementById('editor-textarea').value = e.content;\n document.getElementById('editor-category').value = e.category;\n document.getElementById('editor-date').value = e.date;\n document.getElementById('editor-time').value = e.time;\n document.getElementById('editor-id-label').textContent = e.is_new ? 'New entry' : '';\n document.getElementById('editor-status').textContent = '';\n document.getElementById('btn-save').textContent = e.is_new ? 'Create' : 'Update';\n }\n\n function hideEditor() {\n state._editing = null;\n state.selected_log_id = '';\n document.getElementById('editor-empty').style.display = '';\n document.getElementById('editor-form').style.display = 'none';\n renderLogList();\n }\n\n /* ââ Query details toggle ââ */\n function toggleQueryDetails() {\n var body = document.getElementById('query-body');\n var arrow = document.getElementById('query-arrow');\n var showing = body.style.display !== 'none';\n body.style.display = showing ? 'none' : '';\n arrow.innerHTML = showing ? '▶' : '▼';\n }\n\n function renderQueryDetails() {\n var el = document.getElementById('query-text');\n if (el) el.textContent = state._last_query || '';\n }\n\n /* ââ Reactive rendering ââ */\n SmartChats.app.onRender(function(state, changed) {\n if (changed.has('logs') || changed.has('selected_log_id') || changed.has('loading')) renderLogList();\n if (changed.has('categories')) renderCategories();\n if (changed.has('_last_query')) renderQueryDetails();\n if (changed.has('_editing') || changed.has('selected_log_id')) {\
1n if (state._editing) showEditor();\n else hideEditor();\n }\n });\n\n /* ââ Click handlers ââ */\n function handleFilter() {\n SmartChats.app.fns.load_logs({}, SmartChats.app, SmartChats.util);\n }\n\n function handleSearch() {\n var q = document.getElementById('filter-search').value.trim();\n if (q) {\n SmartChats.app.fns.search({ query: q }, SmartChats.app, SmartChats.util);\n } else {\n handleFilter();\n }\n }\n\n function handleClear() {\n document.getElementById('filter-search').value = '';\n handleFilter();\n }\n\n function handleNewLog() {\n SmartChats.app.fns.new_log({}, SmartChats.app, SmartChats.util);\n }\n\n function handleSave() {\n var content = document.getElementById('editor-textarea').value.trim();\n var category = document.getElementById('editor-category').value;\n var date = document.getElementById('editor-date').value;\n var time = document.getElementById('editor-time').value;\n if (!content) return;\n document.getElementById('btn-save').disabled = true;\n document.getElementById('editor-status').textContent = 'Saving...';\n SmartChats.app.fns.save_edit({\n content: content, category: category, date: date, time: time\n }, SmartChats.app, SmartChats.util)\n .then(function() { document.getElementById('btn-save').disabled = false; })\n .catch(function() { document.getElementById('btn-save').disabled = false; });\n }\n\n function handleCancel() {\n SmartChats.app.setState({ _editing: null, selected_log_id: '' });\n }\n</script>\n"},on_activate:"on_activate",state_schema:{filter_category:{type:"string",default:"",description:"Active category filter",persist:!0},filter_recency:{type:"string",default:"1w",description:"Active recency filter",persist:!0},selected_log_id:{type:"string",default:"",description:"Currently selected log ID",persist:!1},logs:{type:"array",default:[],description:"Currently displayed log entries",persist:!1},categories:{type:"array",default:[],description:"Available log categories",persist:!1},loading:{type:"boolean",default:!1,description:"Whether logs are being loaded",persist:!1}},modules:[{id:"main",name:"Log Explorer",position:60,system_msg:'The Log Explorer app is active. The user can browse, search, and edit their journal logs via the app UI. Available functions:\n- log_explorer_load_logs: Load/refresh logs with current filters (category, recency). Called automatically on open.\n- log_explorer_search: Search logs by text. Pass { query: "search term" }.\n- log_explorer_select_log: Select a log for viewing/editing. Pass { log_id: "..." }.\n- log_explorer_save_edit: Save a new or edited log. Pass { content: "...", category: "..." }.\n- log_explorer_new_log: Open the editor for a new entry. Optionally pass { category: "..." }.\n- log_explorer_load_categories: Refresh the category list.\nThe user can also interact directly via the app UI (tap to select, type to edit, filter dropdowns).',functions:[{name:"on_activate",description:"Initialize: load categories and recent logs",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n await app.fns.load_categories({}, app, util);\n await app.fns.load_logs({}, app, util);\n return { ready: true };\n}"},{name:"load_logs",description:"Load recent logs with current filter settings (category, recency).",parameters:{category:"string",recency:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var catEl = app.dom.getElementById('filter-category');\n var recEl = app.dom.getElementById('filter-recency');\n var limEl = app.dom.getElementById('query-limit');\n var category = catEl && catEl.value !== undefined ? catEl.value : (fnArgs.category || '');\n var recency = recEl && recEl.value !== undefined ? recEl.value : (fnArgs.recency !== undefined ? fnArgs.recency : '1w');\n var limit = limEl ? (Number(limEl.value) || 500) : 500;\n\n app.setState({ loading: true });\n\n var params = { limit: limit };\n if (category) params.category = category;\n if (recency) params.recency = recency;\n\n // Build query description for Query Details panel\n var queryDesc = 'get_recent_logs(' + JSON.stringify(params) + ')';\n app.setState({ _last_query: queryDesc });\n\n var logs = await util.smartchats.get_recent_logs(params);\n\n app.setState({\n logs: Array.isArray(logs) ? logs : [],\n filter_category: category,\n filter_recency: recency,\n loading: false,\n });\n await util.update_workspace({ filter_category: category, filter_recency: recency });\n\n return { count: (app.state.logs || []).length };\n}"},{name:"search",description:"Search logs by text substring.",parameters:{query:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var query = fnArgs.query || '';\n if (!query) return { error: 'query is required' };\n\n app.setState({ loading: true });\n\n var catEl = app.dom.getElementById('filter-category');\n var limEl = app.dom.getElementById('query-limit');\n var category = catEl ? catEl.value : '';\n var limit = limEl ? (Number(limEl.value) || 500) : 500;\n var params = { text: query, limit: limit };\n if (category) params.category = category;\n\n app.setState({ _last_query: 'search_logs(' + JSON.stringify(params) + ')' });\n\n var logs = await util.smartchats.search_logs(params);\n app.setState({ logs: Array.isArray(logs) ? logs : [], loading: false });\n\n return { count: (app.state.logs || []).length, query: query };\n}"},{name:"select_log",description:"Select a log entry for viewing/editing in the editor panel.",parameters:{log_id:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var logId = fnArgs.log_id || fnArgs.id;\n if (!logId) return { error: 'log_id is required' };\n\n var logs = app.state.logs || [];\n var log = null;\n for (var i = 0; i < logs.length;
1 i++) {\n if (logs[i].id === logId) { log = logs[i]; break; }\n }\n if (!log) return { error: 'Log not found in current list' };\n\n var d = parseLogDate(log.ts || log.created_at);\n app.setState({\n selected_log_id: log.id,\n _editing: {\n id: log.id,\n content: log.content || '',\n category: log.category || 'general',\n date: toDateValue(d),\n time: toTimeValue(d),\n is_new: false,\n }\n });\n return { selected: logId, content: log.content, category: log.category };\n}"},{name:"save_edit",description:"Save the current editor content. Updates the existing log if editing (content, category, date, time), creates a new one if new.",parameters:{content:"string",category:"string",date:"string",time:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var content = fnArgs.content;\n var category = fnArgs.category || 'general';\n var date = fnArgs.date || '';\n var time = fnArgs.time || '';\n if (!content) return { error: 'content is required' };\n\n var editing = app.state._editing;\n var result;\n\n if (editing && editing.id && !editing.is_new) {\n // Update existing log\n var params = { id: editing.id, text: content, category: category };\n if (date) { params.date = date; params.time = time || '12:00'; }\n result = await util.smartchats.update_log(params);\n } else {\n // Create new log\n result = await util.smartchats.save_log({ text: content, category: category });\n }\n\n var statusEl = app.dom.getElementById('editor-status');\n statusEl.textContent = editing && editing.id && !editing.is_new ? 'Updated' : 'Created';\n setTimeout(function() {\n if (statusEl) statusEl.textContent = '';\n }, 2000);\n\n // Reload the list\n await app.fns.load_logs({}, app, util);\n\n return result;\n}"},{name:"new_log",description:"Open the editor for creating a new log entry.",parameters:{category:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var now = new Date();\n app.setState({\n selected_log_id: '',\n _editing: {\n id: null,\n content: '',\n category: fnArgs.category || 'general',\n date: toDateValue(now),\n time: toTimeValue(now),\n is_new: true,\n }\n });\n app.dom.getElementById('editor-textarea').focus();\n return { editing: true, is_new: true };\n}"},{name:"load_categories",description:"Refresh the list of available log categories.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var cats = await util.smartchats.get_log_categories();\n app.setState({ categories: Array.isArray(cats) ? cats : [] });\n return { count: (app.state.categories || []).length };\n}"}]}],version_history:[{version:"1.0.0",published_at:"2026-04-07T00:00:00.000Z"},{version:"1.1.0",published_at:new Date().toISOString()}]},I={id:"metrics_explorer",name:"Metrics Explorer",version:"1.3.0",description:"Browse, visualize, and log metric data. View trends, habit streaks, and completion rates. Filter by date range and aggregation mode.",icon:"\uD83D\uDCCA",source:"builtin",categories:["utility","data"],tags:["metrics","habits","tracking","visualization","builtin"],interaction_mode:"hybrid",display_mode:"panel",permissions:h.builtin,requested_functions:["get_metrics_context","retrieve_metrics","display_metrics","retrieve_habit_summary","save_metric"],html_templates:{main:"\n<style>\n * { margin: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body {\n display: flex; flex-direction: column;\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n font-family: var(--sc-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif);\n font-size: 13px;\n }\n\n /* ââ Shared ââ */\n select, input[type=\"text\"], input[type=\"number\"] {\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: 6px;\n padding: 6px 10px;\n font-size: 12px; font-family: inherit; outline: none;\n }\n select:focus, input:focus { border-color: var(--sc-primary, #58a6ff); }\n .btn {\n background: var(--sc-surface, #161b22);\n color: var(--sc-text-muted, #8b949e);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: 6px;\n padding: 6px 14px;\n font-size: 12px; font-weight: 500; font-family: inherit;\n cursor: pointer; transition: all 0.15s;\n }\n .btn:hover { background: var(--sc-border, #30363d); color: var(--sc-text, #e6edf3); }\n .btn-primary { background: var(--sc-primary, #238636); color: #fff; border-color: transparent; }\n .btn-primary:hover { filter: brightness(1.15); }\n .btn:disabled { opacity: 0.4;
1 cursor: not-allowed; }\n\n /* ââ Filters ââ */\n #filters {\n display: flex; gap: 8px; align-items: center;\n padding: 10px 12px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0; flex-wrap: wrap;\n }\n\n /* ââ Content ââ */\n #content { display: flex; flex: 1; overflow: hidden; }\n\n /* ââ Metric List ââ */\n #metric-list-panel {\n width: 40%; min-width: 180px; max-width: 320px;\n overflow-y: auto; border-right: 1px solid var(--sc-border, #30363d);\n display: flex; flex-direction: column;\n }\n #metric-count {\n padding: 6px 12px; font-size: 10px;\n color: var(--sc-text-muted, #8b949e);\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0;\n }\n #metric-list { flex: 1; overflow-y: auto; }\n .metric-item {\n padding: 10px 12px; cursor: pointer;\n border-bottom: 1px solid var(--sc-border, #30363d);\n transition: background 0.12s;\n }\n .metric-item:hover { background: var(--sc-surface, #161b22); }\n .metric-item.selected {\n background: var(--sc-surface-alt, #1c2128);\n border-left: 3px solid var(--sc-primary, #58a6ff);\n padding-left: 9px;\n }\n .metric-name {\n font-size: 13px; font-weight: 600;\n color: var(--sc-text, #e6edf3);\n display: flex; align-items: center; gap: 6px;\n }\n .metric-name .type-badge {\n font-size: 9px; padding: 1px 5px; border-radius: 3px;\n background: var(--sc-accent, #58a6ff); color: #fff;\n text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;\n }\n .metric-name .type-badge.boolean { background: var(--sc-success, #3fb950); }\n .metric-sub {\n font-size: 11px; color: var(--sc-text-muted, #8b949e); margin-top: 2px;\n }\n #metric-list-empty {\n padding: 40px 20px; text-align: center;\n color: var(--sc-text-muted, #8b949e); font-size: 12px;\n }\n #metric-spinner {\n padding: 30px 20px; text-align: center;\n color: var(--sc-text-muted, #8b949e); font-size: 12px;\n display: none;\n }\n\n /* ââ Detail Panel ââ */\n #detail-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow-y: auto; }\n #detail-empty {\n flex: 1; display: flex; align-items: center; justify-content: center;\n color: var(--sc-text-muted, #484f58); font-size: 13px;\n }\n #detail-content { display: none; flex-direction: column; flex: 1; }\n #detail-header {\n padding: 14px 16px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n }\n #detail-header .detail-title {\n font-size: 16px; font-weight: 700; margin-bottom: 8px;\n display: flex; align-items: center; gap: 8px;\n }\n .stats-grid {\n display: flex; gap: 16px; flex-wrap: wrap;\n }\n .stat-item {\n display: flex; flex-direction: column;\n }\n .stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--sc-text-muted, #8b949e); }\n .stat-val { font-size: 14px; font-weight: 600; }\n\n /* ââ Data Table ââ */\n #detail-table-wrap {\n flex: 1; overflow-y: auto; padding: 12px 16px;\n }\n #detail-table {\n width: 100%; border-collapse: collapse; font-size: 12px;\n }\n #detail-table th {\n text-align: left; padding: 6px 10px;\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px;\n color: var(--sc-text-muted, #8b949e);\n border-bottom: 1px solid var(--sc-border, #30363d);\n position: sticky; top: 0;\n background: var(--sc-background, #0d1117);\n }\n #detail-table td {\n padding: 6px 10px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n }\n #detail-table tr:hover td { background: var(--sc-surface, #161b22); }\n #detail-loading {\n padding: 30px 20px; text-align: center;\n color: var(--sc-text-muted, #8b949e); font-size: 12px;\n display: none;\n }\n\n /* ââ Habit Summary ââ */\n #habit-section {\n display: none; padding: 14px 16px;\n border-top: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n }\n .habit-grid {\n display: flex; gap: 20px; flex-wrap: wrap;\n }\n .habit-stat { display: flex; flex-direction: column; }\n .habit-val { font-size: 18px; font-weight: 700; }\n .habit-val.streak { color: var(--sc-warning, #d29922); }\n .habit-val.rate { color: var(--sc-success, #3fb950); }\n\n /* ââ Entry Form ââ */\n #entry-form {\n display: none;\n border-top: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0;\n }\n #entry-fields {\n display: flex; gap: 8px; padding: 10px 12px;
1\n align-items: flex-end; flex-wrap: wrap;\n }\n .entry-group { display: flex; flex-direction: column; gap: 3px; }\n .entry-label {\n font-size: 10px; font-weight: 600; text-transform: uppercase;\n letter-spacing: 0.8px; color: var(--sc-text-muted, #8b949e);\n }\n #entry-status {\n font-size: 11px; color: var(--sc-text-muted, #8b949e);\n padding: 0 12px 8px;\n }\n</style>\n\n<!-- Filters -->\n<div id=\"filters\">\n <select id=\"filter-recency\" onchange=\"handleFilterChange()\">\n <option value=\"1w\">1 week</option>\n <option value=\"2w\">2 weeks</option>\n <option value=\"4w\" selected>4 weeks</option>\n <option value=\"12w\">3 months</option>\n <option value=\"26w\">6 months</option>\n <option value=\"\">All time</option>\n </select>\n <select id=\"filter-agg\" onchange=\"handleFilterChange()\">\n <option value=\"raw\">Raw</option>\n <option value=\"daily_latest\">Daily (latest)</option>\n <option value=\"daily_avg\">Daily (avg)</option>\n <option value=\"daily_sum\">Daily (sum)</option>\n <option value=\"weekly_avg\">Weekly (avg)</option>\n <option value=\"weekly_sum\">Weekly (sum)</option>\n </select>\n <button class=\"btn\" onclick=\"handleRefresh()\">Refresh</button>\n <button class=\"btn\" onclick=\"handleNewEntry()\">+ Metric</button>\n</div>\n\n<!-- Content -->\n<div id=\"content\">\n <!-- Metric List -->\n <div id=\"metric-list-panel\">\n <div id=\"metric-count\"></div>\n <div id=\"metric-spinner\">Loading...</div>\n <div id=\"metric-list\"></div>\n <div id=\"metric-list-empty\" style=\"display:none\">No metrics tracked yet</div>\n </div>\n\n <!-- Detail -->\n <div id=\"detail-panel\">\n <div id=\"detail-empty\">Select a metric to view details</div>\n <div id=\"detail-content\">\n <div id=\"detail-header\">\n <div class=\"detail-title\" id=\"detail-title\"></div>\n <div class=\"stats-grid\" id=\"detail-stats\"></div>\n </div>\n <div id=\"detail-loading\">Loading data...</div>\n <div id=\"detail-table-wrap\">\n <table id=\"detail-table\">\n <thead><tr><th>Date</th><th>Value</th><th>Unit</th></tr></thead>\n <tbody id=\"detail-tbody\"></tbody>\n </table>\n </div>\n <div id=\"habit-section\">\n <div class=\"habit-grid\" id=\"habit-grid\"></div>\n </div>\n </div>\n </div>\n</div>\n\n<!-- Entry Form -->\n<div id=\"entry-form\">\n <div id=\"entry-fields\">\n <div class=\"entry-group\">
1\n <span class=\"entry-label\">Metric Name</span>\n <input type=\"text\" id=\"entry-name\" placeholder=\"e.g. running_distance\" style=\"width:150px\" />\n </div>\n <div class=\"entry-group\">\n <span class=\"entry-label\">Value</span>\n <input type=\"number\" id=\"entry-value\" placeholder=\"0\" style=\"width:80px\" step=\"any\" />\n </div>\n <div class=\"entry-group\">\n <span class=\"entry-label\">Unit</span>\n <input type=\"text\" id=\"entry-unit\" placeholder=\"e.g. miles\" style=\"width:90px\" />\n </div>\n <div class=\"entry-group\">\n <span class=\"entry-label\">Type</span>\n <select id=\"entry-type\" style=\"width:90px\">\n <option value=\"numeric\">Numeric</option>\n <option value=\"boolean\">Boolean</option>\n </select>\n </div>\n <div class=\"entry-group\">\n <span class=\"entry-label\">Category</span>\n <input type=\"text\" id=\"entry-category\" placeholder=\"e.g. exercise\" style=\"width:100px\" />\n </div>\n <button class=\"btn btn-primary\" id=\"btn-save-entry\" onclick=\"handleSaveEntry()\">Save</button>\n <button class=\"btn\" onclick=\"handleCancelEntry()\">Cancel</button>\n </div>\n <div id=\"entry-status\"></div>\n</div>\n\n<script>\n var state = SmartChats.app.state;\n\n /* ââ Helpers ââ */\n function formatDate(dateStr) {\n if (!dateStr) return '';\n var d = new Date(String(dateStr).replace('Z', ''));\n return d.toLocaleDateString();\n }\n\n function formatValue(val) {\n if (val === null || val === undefined) return 'â';\n if (typeof val === 'number') return val % 1 === 0 ? String(val) : val.toFixed(2);\n return String(val);\n }\n\n /* ââ Render metric list ââ */\n function renderMetricList() {\n var metrics = state.tracked_metrics || [];\n var listEl = document.getElementById('metric-list');\n var emptyEl = document.getElementById('metric-list-empty');\n var countEl = document.getElementById('metric-count');\n var spinnerEl = document.getElementById('metric-spinner');\n\n if (state.loading && metrics.length === 0) {\n spinnerEl.style.display = '';\n listEl.innerHTML = '';\n emptyEl.style.display = 'none';\n countEl.textContent = '';\n return;\n }\n spinnerEl.style.display = 'none';\n\n listEl.innerHTML = '';\n countEl.textContent = metrics.length + ' metric' + (metrics.length !== 1 ? 's' : '');\n\n if (metrics.length === 0) { emptyEl.style.display = ''; return; }\n emptyEl.style.display = 'none';\n\n metrics.forEach(function(m) {\n var item = document.createElement('div');\n var isSelected = state.selected_metric === m.metric_name;\n var isBool = m.metric_type === 'boolean';\n item.className = 'metric-item' + (isSelected ? ' selected' : '');\n item.innerHTML = '<div class=\"metric-name\">'\n + '<span>' + m.metric_name.replace(/_/g, ' ') + '</span>'\n + '<span class=\"type-badge' + (isBool ? ' boolean' : '') + '\">' + (isBool ? 'habit' : m.unit || 'num') + '</span>'\n + '</div>'\n + '<div class=\"metric-sub\">'\n + m.entry_count + ' entries'\n + (m.category ? ' \xb7 ' + m.category : '')\n + '</div>';\n item.onclick = function() {\n SmartChats.app.fns.view_metric({ metric_name: m.metric_name }, SmartChats.app, SmartChats.util);\n };\n listEl.appendChild(item);\n });\n }\n\n function highlightSelected() {\n var items = document.querySelectorAll('.metric-item');\n var metrics = state.tracked_metrics || [];\n items.forEach(function(item, i) {\n if (metrics[i]) {\n item.className = 'metric-item' + (state.selected_metric === metrics[i].metric_name ? ' selected' : '');\n }\n });\n }\n\n /* ââ Render detail view ââ */\n function renderDetail() {\n var data = state.metric_data;\n var emptyEl = document.getElementById('detail-empty');\n var contentEl = document.getElementById('detail-content');\n var loadingEl = document.getElementById('detail-loading');\n var tableWrap = document.getElementById('detail-table-wrap');\n\n if (!state.selected_metric) {\n emptyEl.style.display = '';\n contentEl.style.display = 'none';\n return;\n }\n\n emptyEl.style.display = 'none';\n contentEl.style.display = 'flex';\n\n if (state.loading) {\n loadingEl.style.display = '';\n tableWrap.style.display = 'none';\n } else {\n loadingEl.style.display = 'none';\n tableWrap.style.display = '';\n }\n\n if (!data || !data.rows) return;\n\n // Find metric info\n var metrics = state.tracked_metrics || [];\n var info = null;\n for (var i = 0; i < metrics.length;
1 i++) {\n if (metrics[i].metric_name === state.selected_metric) { info = metrics[i]; break; }\n }\n\n // Header\n var isBool = info && info.metric_type === 'boolean';\n document.getElementById('detail-title').innerHTML =\n '<span>' + state.selected_metric.replace(/_/g, ' ') + '</span>'\n + '<span class=\"type-badge' + (isBool ? ' boolean' : '') + '\" style=\"font-size:10px\">' + (isBool ? 'habit' : (data.unit || 'num')) + '</span>';\n\n // Stats\n var statsEl = document.getElementById('detail-stats');\n var rows = data.rows || [];\n var latest = rows.length > 0 ? rows[0].value : null;\n statsEl.innerHTML =\n '<div class=\"stat-item\"><span class=\"stat-label\">Latest</span><span class=\"stat-val\">' + formatValue(latest) + ' ' + (data.unit || '') + '</span></div>'\n + '<div class=\"stat-item\"><span class=\"stat-label\">Entries</span><span class=\"stat-val\">' + data.row_count + '</span></div>'\n + (info ? '<div class=\"stat-item\"><span class=\"stat-label\">Range</span><span class=\"stat-val\">' + formatValue(info.min_value) + ' â ' + formatValue(info.max_value) + '</span></div>' : '')\n + '<div class=\"stat-item\"><span class=\"stat-label\">Aggregation</span><span class=\"stat-val\">' + (data.aggregation || 'raw') + '</span></div>';\n\n // Data table\n var tbody = document.getElementById('detail-tbody');\n tbody.innerHTML = '';\n rows.forEach(function(row) {\n var tr = document.createElement('tr');\n var dateStr = row.day || row.local_date || row.ts || '';\n tr.innerHTML = '<td>' + formatDate(dateStr) + '</td>'\n + '<td>' + formatValue(row.value) + '</td>'\n + '<td>' + (row.unit || data.unit || '') + '</td>';\n tbody.appendChild(tr);\n });\n\n // Habit section\n renderHabit();\n }\n\n /* ââ Render habit summary ââ */\n function renderHabit() {\n var section = document.getElementById('habit-section');\n var summary = state.habit_summary;\n\n if (!summary || !summary.metric_name) {\n section.style.display = 'none';\n return;\n }\n\n section.style.display = '';\n var grid = document.getElementById('habit-grid');\n grid.innerHTML =\n '<div class=\"habit-stat\"><span class=\"stat-label\">Current Streak</span><span class=\"habit-val streak\">' + (summary.current_streak || 0) + ' days</span></div>'\n + '<div class=\"habit-stat\"><span class=\"stat-label\">Longest Streak</span><span class=\"habit-val\">' + (summary.longest_streak || 0) + ' days</span></div>'\n + '<div class=\"habit-stat\"><span class=\"stat-label\">Completion</span><span class=\"habit-val rate\">' + (summary.completion_rate || '0%') + '</span></div>'\n + '<div class=\"habit-stat\"><span class=\"stat-label\">Days Done</span><span class=\"habit-val\">' + (summary.days_done || 0) + ' / ' + (summary.days_in_range || 0) + '</span></div>'\n + (summary.last_done_date ? '<div class=\"habit-stat\"><span class=\"stat-label\">Last Done</span><span class=\"habit-val\">' + summary.last_done_date + '</span></div>' : '');\n }\n\n /* ââ Render entry form ââ */\n function renderEntryForm() {\n var form = document.getElementById('entry-form');\n var ef = state.entry_form;\n if (!ef) { form.style.display = 'none'; return; }\n form.style.display = '';\n document.getElementById('entry-name').value = ef.metric_name || '';\n document.getElementById('entry-value').value = ef.value !== undefined ? ef.value : '';\n document.getElementById('entry-unit').value = ef.unit || '';\n document.getElementById('entry-type').value = ef.metric_type || 'numeric';\n document.getElementById('entry-category').value = ef.category || '';\n document.getElementById('entry-status').textContent = '';\n }\n\n /* ââ Reactive rendering ââ */\n SmartChats.app.onRender(function(state, changed) {\n if (changed.has('tracked_metrics') || changed.has('loading')) renderMetricList();\n if (changed.has('selected_metric') || changed.has('tracked_metrics')) highlightSelected();\n if (changed.has('metric_data') || changed.has('habit_summary') || changed.has('loading') || changed.has('selected_metric')) renderDetail();\n if (changed.has('entry_form')) renderEntryForm();\n });\n\n /* ââ Click handlers ââ */\n function handleFilterChange() {\n if (state.selected_metric) {\n SmartChats.app.fns.view_metric({ metric_name: state.selected_metric }, SmartChats.app, SmartChats.util);\n }\n }\n\n function handleRefresh() {\n SmartChats.app.fns.load_context({}, SmartChats.app, SmartChats.util);\n }\n\n function handleNewEntry() {\n SmartChats.app.fns.new_entry({}, SmartChats.app, SmartChats.util);\n }\n\n function handleSaveEntry() {\n var name = document.getElementById('entry-name').value.trim();\n var value = document.getElementById('entry-value').value;\n var unit = document.getElementById('entry-unit').value.trim();\n var type = document.getElementById('entry-type').value;\n var category = document.getElementById('entry-category').value.trim();\n if (!name) return;\n\n document.getElementById('btn-save-entry').disabled = true;\n document.getElementById('entry-status').textContent = 'Saving...';\n SmartChats.app.fns.save_entry({\n metric_name: name, value: Number(value) || 0, unit: unit,\n metric_type: type, category: category\n }, SmartChats.app, SmartChats.util)\n .then(function() { document.getElementById('btn-save-entry').disabled = false; })\n .catch(function() { document.getElementById('btn-save-entry').disabled = false; });\n }\n\n function handleCancelEntry() {\n SmartChats.app.setState({ entry_form: null });\n }\n</script>\n"},on_activate:"on_activate",state_schema:{filter_recency:{type:"string",default:"4w",description:"Active date range filter",persist:!0},filter_aggregation:{type:"string",default:"raw",description:"Active aggregation mode",persist:!0},tracked_metrics:{type:"array",default:[],description:"All tracked metric summaries",persist:!1},latest_per_metric:{type:"array",default:[],description:"Most recent entry per tracked metric_name",persist:!1},selected_metric:{type:"string",default:"",description:"Currently selected metric name",persist:!1},metric_data:{type:"object",default:{},description:"Retrieved data for selected metric",persist:!1},habit_summary:{type:"object",default:{},description:"Habit summary for selected boolean metric",persist:!1},loading:{type:"boolean",default:!1,description:"Loading state",persist:!1},entry_form:{type:"object",default:null,description:"Entry form state (null = hidden)",persist:!1}},modules:[{id:"main",name:"Metrics Explorer",position:60,system_msg:'The Metrics Explorer app is active. The user can browse, visualize, and log their tracked metrics via the app UI. Available functions:\n- metrics_explorer_load_context: Refresh the list of all tracked metrics. Called automatically on open.\n- metrics_explorer_view_metric: View details for a specific metric. Pass { metric_name: "..." }. Shows data table, stats, and habit summary for boolean metrics.\n- metrics_explorer_save_entry: Log a new metric entry. Pass { metric_name, value, unit, metric_type, category }.\n- metrics_explorer_new_entry: Open the entry form. Optionally pre-fill with { metric_name, value, unit, metric_type, category }.\nThe user can also interact directly via the app UI (tap metrics, change filters, fill forms).',functions:[{name:"on_activate",description:"Initialize: load all tracked metrics",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n await app.fns.load_context({}, app, util);\n return { ready: true };\n}"},{name:"load_context",description:"Load/refresh the list of all tracked metrics with summary stats.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ loading: true });\n var ctx = await util.smartchats.get_metrics_context();\n app.setState({\n tracked_metrics: ctx && ctx.tracked_metrics ? ctx.tracked_metrics : [],\n latest_per_metric: ctx && ctx.latest_per_metric ? ctx.latest_per_metric : [],\n loading: false,\n });\n return { metric_count: (app.state.tracked_metrics || []).length };\n}"},{name:"view_metric",description:"View detailed data for a specific metric. Shows data table, stats, and habit summary for boolean metrics.",parameters:{metric_name:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var metricName = fnArgs.metric_name;\n if (!metricName) return { error: 'metric_name is required' };\n\n app.setState({ selected_metric: metricName, loading: true, habit_summary: {} });\n\n var recency = app.dom.getElementById('filter-recency').value || '4w';\n var aggregation = app.dom.getElementById('filter-agg').value || 'raw';\n\n var params = { metric_name: metricName, aggregation: aggregation };\n if (recency) params.recency = recency;\n\n var data = await util.smartchats.retrieve_metrics(params);\n app.setState({ metric_data: data || {}, loading: false });\n\n await util.update_workspace({ filter_recency: recency, filter_aggregation: aggregation });\n\n // If boolean metric, also fetch habit summary\n var metrics = app.state.tracked_metrics || [];\n var info = null;\n for (var i = 0; i < metrics.length;
1 i++) {\n if (metrics[i].metric_name === metricName) { info = metrics[i]; break; }\n }\n if (info && info.metric_type === 'boolean') {\n var habitParams = { metric_name: metricName };\n if (recency) habitParams.recency = recency;\n var habit = await util.smartchats.retrieve_habit_summary(habitParams);\n app.setState({ habit_summary: habit || {} });\n }\n\n return { metric_name: metricName, row_count: data ? data.row_count : 0 };\n}"},{name:"save_entry",description:"Log a new metric data point.",parameters:{metric_name:"string",value:"number",unit:"string",metric_type:"string",category:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var name = fnArgs.metric_name;\n var value = fnArgs.value;\n var unit = fnArgs.unit || '';\n var type = fnArgs.metric_type || 'numeric';\n var category = fnArgs.category || '';\n if (!name) return { error: 'metric_name is required' };\n\n var params = { metric_name: name, value: Number(value) || 0, unit: unit, metric_type: type };\n if (category) params.category = category;\n\n var result = await util.smartchats.save_metric(params);\n\n var statusEl = app.dom.getElementById('entry-status');\n statusEl.textContent = 'Saved';\n setTimeout(function() { if (statusEl) statusEl.textContent = ''; }, 2000);\n\n // Reload context to reflect new entry\n await app.fns.load_context({}, app, util);\n\n // If we're viewing this metric, refresh the detail view\n if (app.state.selected_metric === name) {\n await app.fns.view_metric({ metric_name: name }, app, util);\n }\n\n return result;\n}"},{name:"new_entry",description:"Open the entry form to log a new metric. Pre-fills from selected metric if available.",parameters:{metric_name:"string",value:"number",unit:"string",metric_type:"string",category:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var prefill = {\n metric_name: fnArgs.metric_name || app.state.selected_metric || '',\n value: fnArgs.value || '',\n unit: fnArgs.unit || '',\n metric_type: fnArgs.metric_type || 'numeric',\n category: fnArgs.category || '',\n };\n\n // Try to pre-fill unit and type from existing metric info\n if (prefill.metric_name) {\n var metrics = app.state.tracked_metrics || [];\n for (var i = 0; i < metrics.length; i++) {\n if (metrics[i].metric_name === prefill.metric_name) {\n prefill.unit = prefill.unit || metrics[i].unit || '';\n prefill.metric_type = metrics[i].metric_type || prefill.metric_type;\n prefill.category = prefill.category || metrics[i].category || '';\n break;\n }\n }\n }\n\n app.setState({ entry_form: prefill });\n return { editing: true };\n}"},{name:"dom_check",description:"Check DOM state against app state. Returns whether metrics rendered, detail visible, etc.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var metricItems = app.dom.querySelectorAll('.metric-item').length;\n var expectedMetrics = (app.state.tracked_metrics || []).length;\n var detailVisible = app.dom.getElementById('detail-content').style.display !== 'none';\n var detailEmpty = app.dom.getElementById('detail-empty').style.display !== 'none';\n var spinnerVisible = app.dom.getElementById('metric-spinner').style.display !== 'none';\n var entryFormVisible = app.dom.getElementById('entry-form').style.display !== 'none';\n var tableRows = app.dom.querySelectorAll('#detail-tbody tr').length;\n var habitVisible = app.dom.getElementById('habit-section').style.display !== 'none';\n\n return {\n metrics_rendered: metricItems,\n metrics_in_state: expectedMetrics,\n metrics_match: metricItems === expectedMetrics,\n detail_visible: detailVisible,\n detail_empty: detailEmpty,\n spinner_visible: spinnerVisible,\n entry_form_visible: entryFormVisible,\n table_rows: tableRows,\n habit_visible: habitVisible,\n selected_metric: app.state.selected_metric || null,\n loading: !!app.state.loading,\n render_fn_registered: typeof app._pendingRenderFn === 'function' || true,\n };\n}"}]}],version_history:[{version:"1.0.0",published_at:"2026-04-07T00:00:00.000Z"},{version:"1.1.0",published_at:new Date().toISOString()}]},A={id:"todo",name:"Todo Manager",version:"1.0.0",description:"Browse, create, complete, edit, and delete todos. Organized by urgency: overdue, due today, upcoming, no date, and recurring due.",icon:"â
",source:"builtin",categories:["utility","productivity"],tags:["todos","tasks","productivity","builtin"],interaction_mode:"hybrid",display_mode:"panel",permissions:h.builtin,requested_functions:["get_todos_context","save_todo","manage_todo"],html_templates:{main:"\n<style>\n * { margin: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body {\n display: flex; flex-direction: column;\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n font-family: var(--sc-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif);\n font-size: 13px;\n }\n\n /* ââ Shared Controls ââ */\n select, input[type=\"text\"], input[type=\"date\"] {\n background: var(--sc-background, #0d1117);\n color: var(--sc-text, #e6edf3);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: var(--sc-radius-sm, 4px);\n padding: 4px 8px;\n font-size: 12px;\n font-family: inherit;\n outline: none;\n }\n select:focus, input:focus {\n border-color: var(--sc-primary, #58a6ff);\n }\n button {\n cursor: pointer; border: none; font-family: inherit; font-size: 12px;\n border-radius: var(--sc-radius-sm, 4px);\n transition: background 0.15s, opacity 0.15s;\n }\n button:active { opacity: 0.7; }\n\n .btn-primary {\n background: var(--sc-primary, #58a6ff); color: #fff; padding: 5px 14px; font-weight: 600;\n }\n .btn-secondary {\n background: var(--sc-surface, #161b22); color: var(--sc-text-muted, #8b949e); padding: 5px 14px;\n border: 1px solid var(--sc-border, #30363d);\n }\n .btn-icon {\n background: transparent; color: var(--sc-text-muted, #8b949e); padding: 4px;\n display: flex; align-items: center; justify-content: center;\n }\n .btn-icon:hover { color: var(--sc-text, #e6edf3); }\n .btn-icon.danger:hover { color: var(--sc-danger, #f85149); }\n\n /* ââ Filter Bar ââ */\n #filters {\n display: flex; align-items: center; gap: 8px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n flex-shrink: 0;\n }\n #filters select { flex: 0 0 auto; min-width: 100px; }\n #filters .spacer { flex: 1; }\n\n /* ââ Create/Edit Form ââ */\n #create-form {\n display: none;\n padding: 12px 14px;\n border-bottom: 1px solid var(--sc-border, #30363d);\n background: var(--sc-surface, #161b22);\n flex-shrink: 0;\n }\n #create-form.visible { display: block; }\n .form-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }\n .form-row:last-child { margin-bottom: 0; }\n .form-row label { font-size: 11px; color: var(--sc-text-muted, #8b949e); min-width: 55px; }\n .form-row input, .form-row select { flex: 1; }\n #form-title { flex: 2; }\n .form-heading {\n font-size: 11px; font-weight: 600; text-transform: uppercase;\n letter-spacing: 0.05em; color: var(--sc-primary, #58a6ff);\n margin-bottom: 8px;\n }\n\n /* ââ Todo List ââ */\n #todo-list {\n flex: 1; overflow-y: auto; padding: 10px 14px;\n }
1\n #todo-empty {\n display: none;\n text-align: center; color: var(--sc-text-muted, #8b949e);\n padding: 40px 0; font-size: 13px;\n }\n #todo-spinner {\n display: none;\n text-align: center; color: var(--sc-text-muted, #8b949e);\n padding: 30px 0; font-size: 12px;\n }\n\n /* ââ Sections ââ */\n .section { margin-bottom: 14px; }\n .section:last-child { margin-bottom: 0; }\n .section-header {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; margin-bottom: 6px;\n }\n .section-header.overdue { color: var(--sc-danger, #f85149); }\n .section-header.due-today { color: var(--sc-primary, #58a6ff); }\n .section-header.upcoming { color: var(--sc-text-muted, #8b949e); }\n .section-header.no-date { color: var(--sc-text-muted, #8b949e); }\n .section-header.recurring { color: var(--sc-accent, #bc8cff); }\n\n /* ââ Todo Item ââ */\n .todo-item, .recurring-item {\n display: flex; align-items: center; gap: 8px;\n padding: 7px 10px;\n background: var(--sc-surface, #161b22);\n border: 1px solid var(--sc-border, #30363d);\n border-radius: var(--sc-radius-sm, 4px);\n margin-bottom: 4px;\n transition: opacity 0.2s;\n }\n .todo-item:hover .todo-actions, .recurring-item:hover .todo-actions {\n opacity: 1;\n }\n .todo-item.completed { opacity: 0.45; }\n\n /* ââ Checkbox ââ */\n .todo-checkbox {\n width: 18px; height: 18px; flex-shrink: 0;\n border: 1.5px solid var(--sc-border, #30363d);\n border-radius: 3px; background: transparent;\n display: flex; align-items: center; justify-content: center;\n cursor: pointer; transition: all 0.15s; padding: 0;\n }\n .todo-checkbox:hover { border-color: var(--sc-primary, #58a6ff); }\n .todo-checkbox.checked {\n background: var(--sc-success, #3fb950);\n border-color: transparent;\n }\n .todo-checkbox svg { display: none; }\n .todo-checkbox.checked svg { display: block; }\n\n /* ââ Title / Meta ââ */\n .todo-title {\n flex: 1; min-width: 0;\n font-size: 12px; color: var(--sc-text, #e6edf3);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .todo-item.completed .todo-title { text-decoration: line-through; }\n .todo-due {\n font-size: 10px; color: var(--sc-text-muted, #8b949e); flex-shrink: 0;\n }\n .todo-priority {\n font-size: 9px; font-weight: 600; text-transform: uppercase;\n padding: 2px 6px; border-radius: 3px; flex-shrink: 0;\n }\n .todo-priority.urgent { background: var(--sc-danger, #f85149); color: #fff; }\n .todo-priority.high { background: var(--sc-warning, #d29922); color: #fff; }\n .todo-priority.medium { background: var(--sc-primary, #58a6ff); color: #fff; }\n .todo-priority.low { background: var(--sc-surface-alt, #21262d); color: var(--sc-text-muted, #8b949e); }\n\n /* ââ Recurring ââ */\n .recurring-progress {\n font-size: 10px; color: var(--sc-text-muted, #8b949e); flex-shrink: 0;\n }\n .recurring-pattern {\n font-size: 9px; font-weight: 600; text-transform: uppercase;\n padding: 2px 6px; border-radius: 3px; flex-shrink: 0;\n background: var(--sc-accent, #bc8cff); color: #fff;\n }\n\n /* ââ Actions ââ */\n .todo-actions {\n display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; flex-shrink: 0;\n }\n\n /* ââ Status Bar ââ */\n #status-bar {\n padding: 6px 14px;\n border-top: 1px solid var(--sc-border, #30363d);\n font-size: 11px; color: var(--sc-text-muted, #8b949e);\n flex-shrink: 0;\n }\n\n /* ââ Error ââ */\n #error-bar {\n display: none; padding: 6px 14px;\n background: var(--sc-danger, #f85149); color: #fff;\n font-size: 11px; flex-shrink: 0;\n }\n #error-bar.visible { display: block; }\n</style>\n\n<!-- ââ Filter Bar ââ -->\n<div id=\"filters\">\n <select id=\"filter-category\"><option value=\"\">All categories</option></select>\n <button class=\"btn-icon\" id=\"btn-refresh\" title=\"Refresh\">↻</button>\n <span class=\"spacer\"></span>\n <button class=\"btn-primary\" id=\"btn-new\">+ Todo</button>\n</div>\n\n<!-- ââ Create / Edit Form ââ -->\n<div id=\"create-form\">\n <div class=\"form-heading\" id=\"form-heading\">New Todo</div>\n <div class=\"form-row\">\n <label>Title</label>\n <input type=\"text\" id=\"form-title\" placeholder=\"What needs to be done?\" />\n </div>\n <div class=\"form-row\">\n <label>Priority</label>\n <select id=\"form-priority\">\n <option value=\"low\">Low</option>\n <option value=\"medium\" selected>Medium</option>\n <option value=\"high\">High</option>\n <option value=\"urgent\">Urgent</option>\n </select>\n <label>Category</label>\n <input type=\"text\" id=\"form-category\" placeholder=\"general\" />\n </div>\n <div class=\"form-row\">\n <label>Due date</label>\n <input type=\"date\" id=\"form-due-date\" />\n <span class=\"spacer\"></span>\n <button class=\"btn-primary\" id=\"btn-save-form\">Save</button>\n <button class=\"btn-secondary\" id=\"btn-cancel-form\">Cancel</button>\n </div>\n</div>\n\n<!-- ââ Error Bar ââ -->\n<div id=\"error-bar\"></div>\n\n<!-- ââ Todo List ââ -->\n<div id=\"todo-list\">\n <div id=\"todo-spinner\">Loading todos...</div>\n <div id=\"todo-empty\">No todos right now. Tap + Todo to create one.</div>\n <div id=\"todo-sections\"></div>\n</div>\n\n<!-- ââ Status Bar ââ -->\n<div id=\"status-bar\">\n <span id=\"total-count\">0 active todos</span>\n</div>\n\n<script>\n// ââ Render helpers ââ\n\nfunction formatDueDate(iso) {\n if (!iso) return '';\n var d = new Date(iso);\n var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\n return months[d.getUTCMonth()] + ' ' + d.getUTCDate();\n}\n\nfunction makeTodoRow(item, isCompleted) {\n var row = document.createElement('div');\n row.className = 'todo-item' + (isCompleted ? ' completed' : '');\n row.dataset.id = item.id;\n\n var cb = document.createElement('button');\n cb.className = 'todo-checkbox' + (isCompleted ? ' checked' : '');\n cb.innerHTML = '<svg width=\"10\" height=\"8\" viewBox=\"0 0 10 8\" fill=\"none\">
1<path d=\"M1 4L3.5 6.5L9 1\" stroke=\"#fff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>';\n cb.onclick = function() {\n SmartChats.app.fns.complete_todo({ id: item.id, is_recurring: false }, SmartChats.app, SmartChats.util);\n };\n\n var title = document.createElement('span');\n title.className = 'todo-title';\n title.textContent = item.title;\n\n row.appendChild(cb);\n row.appendChild(title);\n\n if (item.due_date) {\n var due = document.createElement('span');\n due.className = 'todo-due';\n due.textContent = formatDueDate(item.due_date);\n row.appendChild(due);\n }\n\n if (item.priority) {\n var pri = document.createElement('span');\n pri.className = 'todo-priority ' + item.priority;\n pri.textContent = item.priority;\n row.appendChild(pri);\n }\n\n var actions = document.createElement('span');\n actions.className = 'todo-actions';\n\n var editBtn = document.createElement('button');\n editBtn.className = 'btn-icon';\n editBtn.innerHTML = '✎';\n editBtn.title = 'Edit';\n editBtn.onclick = function(e) {\n e.stopPropagation();\n SmartChats.app.fns.edit_todo({ id: item.id }, SmartChats.app, SmartChats.util);\n };\n\n var delBtn = document.createElement('button');\n delBtn.className = 'btn-icon danger';\n delBtn.innerHTML = '🗑';\n delBtn.title = 'Delete';\n delBtn.onclick = function(e) {\n e.stopPropagation();\n SmartChats.app.fns.delete_todo({ id: item.id }, SmartChats.app, SmartChats.util);\n };\n\n actions.appendChild(editBtn);\n actions.appendChild(delBtn);\n row.appendChild(actions);\n\n return row;\n}\n\nfunction makeRecurringRow(item, isCompleted) {\n var row = document.createElement('div');\n row.className = 'recurring-item' + (isCompleted ? ' completed' : '');\n row.dataset.id = item.id;\n\n var cb = document.createElement('button');\n cb.className = 'todo-checkbox' + (isCompleted ? ' checked' : '');\n cb.innerHTML = '<svg width=\"10\" height=\"8\" viewBox=\"0 0 10 8\" fill=\"none\">
1<path d=\"M1 4L3.5 6.5L9 1\" stroke=\"#fff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>';\n cb.onclick = function() {\n SmartChats.app.fns.complete_todo({ id: item.id, is_recurring: true }, SmartChats.app, SmartChats.util);\n };\n\n var title = document.createElement('span');\n title.className = 'todo-title';\n title.textContent = item.title;\n\n var progress = document.createElement('span');\n progress.className = 'recurring-progress';\n var done = item.done_this_period + (isCompleted ? 1 : 0);\n progress.textContent = item.target ? Math.min(done, item.target) + '/' + item.target : done + ' done';\n\n var pattern = document.createElement('span');\n pattern.className = 'recurring-pattern';\n pattern.textContent = item.pattern;\n\n var actions = document.createElement('span');\n actions.className = 'todo-actions';\n var delBtn = document.createElement('button');\n delBtn.className = 'btn-icon danger';\n delBtn.innerHTML = '🗑';\n delBtn.title = 'Delete';\n delBtn.onclick = function(e) {\n e.stopPropagation();\n SmartChats.app.fns.delete_todo({ id: item.id }, SmartChats.app, SmartChats.util);\n };\n actions.appendChild(delBtn);\n\n row.appendChild(cb);\n row.appendChild(title);\n row.appendChild(progress);\n row.appendChild(pattern);\n row.appendChild(actions);\n\n return row;\n}
1\n\nfunction renderSection(container, sectionClass, label, items, completedIds, makeRow) {\n if (!items || items.length === 0) return;\n var section = document.createElement('div');\n section.className = 'section';\n section.dataset.section = sectionClass;\n\n var header = document.createElement('div');\n header.className = 'section-header ' + sectionClass;\n header.textContent = label;\n section.appendChild(header);\n\n for (var i = 0; i < items.length; i++) {\n var isComp = completedIds.indexOf(items[i].id) !== -1;\n section.appendChild(makeRow(items[i], isComp));\n }\n container.appendChild(section);\n}\n\nfunction renderTodoList() {\n var state = SmartChats.app.state;\n var todos = state.todos || {};\n var completedIds = state.completed_ids || [];\n var loading = state.loading;\n\n var spinner = SmartChats.app.el('#todo-spinner');\n var empty = SmartChats.app.el('#todo-empty');\n var sections = SmartChats.app.el('#todo-sections');\n\n spinner.style.display = loading ? 'block' : 'none';\n if (loading) {\n empty.style.display = 'none';\n sections.innerHTML = '';\n return;\n }\n\n sections.innerHTML = '';\n\n var overdue = todos.overdue || [];\n var dueToday = todos.due_today || [];\n var upcoming = todos.upcoming_7d || [];\n var noDate = todos.no_date || [];\n var recurring = todos.recurring_due || [];\n\n var totalItems = overdue.length + dueToday.length + upcoming.length + noDate.length + recurring.length;\n\n if (totalItems === 0) {\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n renderSection(sections, 'overdue', 'Overdue', overdue, completedIds, makeTodoRow);\n renderSection(sections, 'due-today', 'Due Today', dueToday, completedIds, makeTodoRow);\n renderSection(sections, 'upcoming', 'Upcoming', upcoming, completedIds, makeTodoRow);\n renderSection(sections, 'no-date', 'No Due Date', noDate, completedIds, makeTodoRow);\n renderSection(sections, 'recurring', 'Recurring Due', recurring, completedIds, makeRecurringRow);\n\n // Populate category filter\n var cats = {};\n var allItems = [].concat(overdue, dueToday, upcoming, noDate);\n for (var i = 0; i < allItems.length; i++) {\n if (allItems[i].category) cats[allItems[i].category] = true;\n }\n var sel = SmartChats.app.el('#filter-category');\n var curVal = sel.value;\n sel.innerHTML = '<option value=\"\">All categories</option>';\n var catKeys = Object.keys(cats).sort();\n for (var c = 0; c < catKeys.length; c++) {\n var opt = document.createElement('option');\n opt.value = catKeys[c];\n opt.textContent = catKeys[c];\n if (catKeys[c] === curVal) opt.selected = true;\n sel.appendChild(opt);\n }\n}\n\nfunction renderCreateForm() {\n var state = SmartChats.app.state;\n var form = SmartChats.app.el('#create-form');\n var heading = SmartChats.app.el('#form-heading');\n\n if (!state.create_form) {\n form.classList.remove('visible');\n return;\n }\n form.classList.add('visible');\n\n var isEditing = !!(state.editing_id);\n heading.textContent = isEditing ? 'Edit Todo' : 'New Todo';\n\n var cf = state.create_form;\n SmartChats.app.el('#form-title').value = cf.title || '';\n SmartChats.app.el('#form-priority').value = cf.priority || 'medium';\n SmartChats.app.el('#form-category').value = cf.category || '';\n SmartChats.app.el('#form-due-date').value = cf.due_date || '';\n}
1\n\nfunction renderStatusBar() {\n var state = SmartChats.app.state;\n var total = (state.todos && state.todos.total_active) || 0;\n SmartChats.app.el('#total-count').textContent = total + ' active todo' + (total !== 1 ? 's' : '');\n}\n\nfunction renderError() {\n var state = SmartChats.app.state;\n var bar = SmartChats.app.el('#error-bar');\n if (state.error) {\n bar.textContent = state.error;\n bar.classList.add('visible');\n setTimeout(function() {\n SmartChats.app.setState({ error: '' });\n }, 4000);\n } else {\n bar.classList.remove('visible');\n }\n}\n\n// ââ onRender ââ\n\nSmartChats.app.onRender(function(state, changed) {\n if (changed.has('todos') || changed.has('loading') || changed.has('completed_ids')) renderTodoList();\n if (changed.has('create_form') || changed.has('editing_id')) renderCreateForm();\n if (changed.has('todos')) renderStatusBar();\n if (changed.has('error')) renderError();\n});\n\n// ââ UI Event Listeners ââ\n\ndocument.getElementById('btn-new').onclick = function() {\n SmartChats.app.fns.open_create_form({}, SmartChats.app, SmartChats.util);\n};\n\ndocument.getElementById('btn-refresh').onclick = function() {\n SmartChats.app.fns.load_todos({}, SmartChats.app, SmartChats.util);\n};\n\ndocument.getElementById('btn-save-form').onclick = function() {\n var state = SmartChats.app.state;\n var title = document.getElementById('form-title').value;\n var priority = document.getElementById('form-priority').value;\n var category = document.getElementById('form-category').value;\n var due_date = document.getElementById('form-due-date').value;\n\n if (state.editing_id) {\n SmartChats.app.fns.save_edit({ title: title, priority: priority, category: category, due_date: due_date }, SmartChats.app, SmartChats.util);\n } else {\n SmartChats.app.fns.create_todo({ title: title, priority: priority, category: category, due_date: due_date }, SmartChats.app, SmartChats.util);\n }\n};\n\ndocument.getElementById('btn-cancel-form').onclick = function() {\n SmartChats.app.setState({ create_form: null, editing_id: '' });\n};\n\ndocument.getElementById('filter-category').onchange = function() {\n SmartChats.app.setState({ filter_category: this.value });\n SmartChats.app.fns.load_todos({}, SmartChats.app, SmartChats.util);\n};\n</script>\n"},on_activate:"on_activate",state_schema:{filter_category:{type:"string",default:"",description:"Active category filter",persist:!0},todos:{type:"object",default:{},description:"Categorized todos from get_todos_context",persist:!1},loading:{type:"boolean",default:!1,description:"Loading state",persist:!1},create_form:{type:"object",default:null,description:"Create/edit form state (null = hidden)",persist:!1},editing_id:{type:"string",default:"",description:"ID of todo being edited",persist:!1},completed_ids:{type:"array",default:[],description:"Optimistically completed todo IDs",persist:!1},error:{type:"string",default:"",description:"Error message",persist:!1}},modules:[{id:"main",name:"Todo Manager",position:60,functions:[{name:"on_activate",description:"Load todos on activation.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n return app.fns.load_todos({}, app, util);\n}"},{name:"load_todos",description:"Fetch categorized todos from the database. Respects the current category filter.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ loading: true, completed_ids: [] });\n try {\n var category = app.state.filter_category || undefined;\n var ctx = await util.smartchats.get_todos_context({ category: category });\n app.setState({ todos: ctx, loading: false });\n return { ok: true, total: ctx.total_active || 0 };\n } catch (e) {\n app.setState({ loading: false, error: 'Failed to load todos: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"create_todo",description:"Create a new todo. Returns the saved todo ID.",parameters:{title:"string",priority:"string",category:"string",due_date:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var title = fnArgs.title;\n if (!title || !title.trim()) {\n app.setState({ error: 'Title is required' });\n return { ok: false, error: 'title required' };\n }\n try {\n var result = await util.smartchats.save_todo({\n title: title.trim(),\n priority: fnArgs.priority || 'medium',\n category: fnArgs.category || 'general',\n due_date: fnArgs.due_date || undefined,\n });\n app.setState({ create_form: null, editing_id: '' });\n await app.fns.load_todos({}, app, util);\n return { ok: true, id: result.id, title: title.trim() };\n } catch (e) {\n app.setState({ error: 'Failed to create todo: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"complete_todo",description:"Mark a todo as complete. If no id provided, completes the first visible todo.",parameters:{id:"string",is_recurring:"boolean"},return_type:"object",code:"async function(fnArgs, app, util) {\n var id = fnArgs.id;\n if (!id) {\n // Use first visible todo from state\n var todos = app.state.todos || {};\n var all = [].concat(todos.overdue || [], todos.due_today || [], todos.upcoming_7d || [], todos.no_date || [], todos.recurring_due || []);
1\n if (all.length === 0) return { ok: false, error: 'No todos to complete' };\n id = all[0].id;\n }\n var isRecurring = !!(fnArgs.is_recurring);\n\n // Optimistic update\n var ids = (app.state.completed_ids || []).slice();\n if (ids.indexOf(id) === -1) ids.push(id);\n app.setState({ completed_ids: ids });\n\n try {\n await util.smartchats.manage_todo({ id: id, action: 'complete' });\n await app.fns.load_todos({}, app, util);\n return { ok: true, id: id };\n } catch (e) {\n // Revert\n var reverted = (app.state.completed_ids || []).filter(function(x) { return x !== id; });\n app.setState({ completed_ids: reverted, error: 'Failed to complete: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"delete_todo",description:"Delete a todo by ID.",parameters:{id:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var id = fnArgs.id;\n if (!id) return { ok: false, error: 'id required' };\n try {\n await util.smartchats.manage_todo({ id: id, action: 'delete' });\n await app.fns.load_todos({}, app, util);\n return { ok: true, id: id };\n } catch (e) {\n app.setState({ error: 'Failed to delete: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"edit_todo",description:"Open the edit form pre-populated with a todo's data.",parameters:{id:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var id = fnArgs.id;\n if (!id) return { ok: false, error: 'id required' };\n\n // Find the todo across all sections\n var todos = app.state.todos || {};\n var allItems = [].concat(todos.overdue || [], todos.due_today || [], todos.upcoming_7d || [], todos.no_date || []);\n var found = null;\n for (var i = 0; i < allItems.length; i++) {\n if (allItems[i].id === id) { found = allItems[i]; break; }\n }\n if (!found) return { ok: false, error: 'Todo not found in state' };\n\n app.setState({\n create_form: {\n title: found.title || '',\n priority: found.priority || 'medium',\n category: found.category || '',\n due_date: found.due_date || '',\n },\n editing_id: id,\n });\n return { ok: true };\n}"},{name:"save_edit",description:"Save edits to the currently-editing todo.",parameters:{title:"string",priority:"string",category:"string",due_date:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var id = app.state.editing_id;\n if (!id) return { ok: false, error: 'No todo being edited' };\n var title = fnArgs.title;\n if (!title || !title.trim()) {\n app.setState({ error: 'Title is required' });\n return { ok: false, error: 'title required' };\n }\n try {\n await util.smartchats.manage_todo({\n id: id,\n action: 'edit',\n updates: {\n title: title.trim(),\n priority: fnArgs.priority || 'medium',\n category: fnArgs.category || 'general',\n due_date: fnArgs.due_date || undefined,\n },\n });\n app.setState({ create_form: null, editing_id: '' });\n await app.fns.load_todos({}, app, util);\n return { ok: true, id: id };\n } catch (e) {\n app.setState({ error: 'Failed to save edit: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"open_create_form",description:"Open the create form for a new todo.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({\n create_form: { title: '', priority: 'medium', category: '', due_date: '' },\n editing_id: '',\n });\n return { ok: true };\n}"},{name:"seed_test_todos",description:"Create test todos across all urgency buckets. For automated testing only.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var now = Date.now();\n var day = 86400000;\n var ids = [];\n\n function dateStr(offset) {\n return new Date(now + offset).toISOString().slice(0, 10);\n }\n\n var todos = [\n { title: '__simi_overdue_1', priority: 'high', category: 'test', due_date: dateStr(-1 * day) },\n { title: '__simi_overdue_2', priority: 'urgent', category: 'test', due_date: dateStr(-3 * day) },\n { title: '__simi_today_1', priority: 'medium', category: 'test', due_date: dateStr(0) },\n { title: '__simi_today_2', priority: 'high', category: 'work', due_date: dateStr(0) },\n { title: '__simi_upcoming_1', priority: 'medium', category: 'test', due_date: dateStr(3 * day) },\n { title: '__simi_upcoming_2', priority: 'low', category: 'personal', due_date: dateStr(5 * day) },\n { title: '__simi_nodate_1', priority: 'low', category: 'test' },\n ];\n\n for (var i = 0; i < todos.length;
1 i++) {\n try {\n var result = await util.smartchats.save_todo(todos[i]);\n if (result && result.id) ids.push(result.id);\n } catch (e) {\n util.log('seed_test_todos: failed to save ' + todos[i].title + ': ' + (e.message || e));\n }\n }\n\n return { seeded: true, count: ids.length, ids: ids };\n}"},{name:"dom_check",description:"Check DOM state against app state. Returns whether UI elements match the data.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var todos = app.state.todos || {};\n var completedIds = app.state.completed_ids || [];\n\n var overdueItems = app.dom.querySelectorAll('.section[data-section=\"overdue\"] .todo-item');\n var dueTodayItems = app.dom.querySelectorAll('.section[data-section=\"due-today\"] .todo-item');\n var upcomingItems = app.dom.querySelectorAll('.section[data-section=\"upcoming\"] .todo-item');\n var noDateItems = app.dom.querySelectorAll('.section[data-section=\"no-date\"] .todo-item');\n var recurringItems = app.dom.querySelectorAll('.section[data-section=\"recurring\"] .recurring-item');\n\n var overdueState = (todos.overdue || []).length;\n var dueTodayState = (todos.due_today || []).length;\n var upcomingState = (todos.upcoming_7d || []).length;\n var noDateState = (todos.no_date || []).length;\n var recurringState = (todos.recurring_due || []).length;\n\n var totalRendered = overdueItems.length + dueTodayItems.length + upcomingItems.length + noDateItems.length + recurringItems.length;\n var totalState = overdueState + dueTodayState + upcomingState + noDateState + recurringState;\n\n // Find first todo ID in state for test use\n var firstId = null;\n var allItems = [].concat(todos.overdue || [], todos.due_today || [], todos.upcoming_7d || [], todos.no_date || [], todos.recurring_due || []);\n if (allItems.length > 0) firstId = allItems[0].id;\n\n var completedEls = app.dom.querySelectorAll('.todo-item.completed');\n var formEl = app.dom.getElementById('create-form');\n var emptyEl = app.dom.getElementById('todo-empty');\n var statusEl = app.dom.getElementById('total-count');\n\n return {\n overdue_rendered: overdueItems.length,\n overdue_in_state: overdueState,\n overdue_match: overdueItems.length === overdueState,\n\n due_today_rendered: dueTodayItems.length,\n due_today_in_state: dueTodayState,\n due_today_match: dueTodayItems.length === dueTodayState,\n\n upcoming_rendered: upcomingItems.length,\n upcoming_in_state: upcomingState,\n upcoming_match: upcomingItems.length === upcomingState,\n\n no_date_rendered: noDateItems.length,\n no_date_in_state: noDateState,\n no_date_match: noDateItems.length === noDateState,\n\n recurring_rendered: recurringItems.length,\n recurring_in_state: recurringState,\n recurring_match: recurringItems.length === recurringState,\n\n total_rendered: totalRendered,\n total_in_state: totalState,\n total_match: totalRendered === totalState,\n\n loading: !!app.state.loading,\n create_form_visible: formEl ? formEl.classList.contains('visible') : false,\n empty_visible: emptyEl ? emptyEl.style.display !== 'none' : false,\n status_bar_text: statusEl ? statusEl.textContent : '',\n completed_count: completedEls.length,\n completed_ids_in_state: completedIds.length,\n first_todo_id: firstId,\n };\n}"}]}],version_history:[{version:"1.0.0",published_at:new Date().toISOString()}]};var B=n(86054);let R='\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n display: flex; flex-direction: column;\n padding: 4vmin; gap: 3vmin; height: 100%;\n font-family: var(--sc-font, system-ui, sans-serif);\n background: var(--sc-background, #111); color: var(--sc-text, #eee);\n }\n\n /* Toolbar */\n #toolbar {\n display: flex; gap: 8px; align-items: center;\n }\n #toolbar input[type="text"] {\n flex: 1; padding: 8px 12px; border-radius: var(--sc-radius-md, 8px);\n border: 1px solid var(--sc-border, #333); background: var(--sc-surface, #1a1a1a);\n color: var(--sc-text, #eee); font-size: 14px; outline: none;\n }\n #toolbar input[type="text"]:focus { border-color: var(--sc-accent, #6366f1); }\n .btn {\n padding: 8px 14px; border-radius: var(--sc-radius-md, 8px); border: none;\n background: var(--sc-accent, #6366f1); color: #fff; font-size: 13px;\n cursor: pointer; white-space: nowrap; transition: opacity 0.2s;\n }\n .btn:hover { opacity: 0.85; }\n .btn-muted {\n background: var(--sc-surface-alt, #252525); color: var(--sc-text-muted, #888);\n }\n .btn-danger { background: var(--sc-danger, #ef4444); }\n .btn-sm { padding: 4px 10px; font-size: 12px; }\n\n /* Add form */\n #add-form {\n padding: 12px; border-radius: var(--sc-radius-md, 8px);\n border: 1px solid var(--sc-border, #333); background: var(--sc-surface, #1a1a1a);\n display: none;\n }\n #add-form .form-row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }\n #add-form label { font-size: 12px; color: var(--sc-text-muted, #888); min-width: 60px; }\n #add-form input {\n flex: 1; padding: 6px 10px; border-radius: 6px;\n border: 1px solid var(--sc-border, #333); background: var(--sc-background, #111);\n color: var(--sc-text, #eee); font-size: 13px; outline: none;\n }\n #add-form .form-actions { display: flex; gap: 8px; justify-content: flex-end; }\n\n /* Scrollable content area */\n #content { flex: 1; overflow-y: auto; }\n\n /* Entity list */\n #entity-list { display: flex; flex-direction: column; gap: 4px; }\n .entity-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: var(--sc-radius-md, 8px);\n border: 1px solid var(--sc-border, #333); background: var(--sc-surface, #1a1a1a);\n cursor: pointer; transition: all 0.15s;\n }\n .entity-row:hover {\n border-color: var(--sc-accent, #6366f1);\n background: color-mix(in srgb, var(--sc-accent, #6366f1) 6%, var(--sc-surface, #1a1a1a));\n }\n .entity-name { font-size: 14px; color: var(--sc-text, #eee); }\n .entity-count { font-size: 12px; color: var(--sc-text-muted, #888); }\n .entity-arrow { color: var(--sc-text-muted, #888); font-size: 14px; }\n\n /* Graph container */\n #graph-container {\n width: 100%; height: 280px;\n border-radius: var(--sc-radius-md, 8px);\n border: 1px solid var(--sc-border, #333);\n background: var(--sc-background, #111);\n margin-bottom: 12px; display: none;\n position: relative; overflow: hidden;\n }\n .sigma-container { width: 100% !important; height: 100% !important; position: relative !important; }\n .sigma-container canvas { position: absolute !important; top: 0 !important; left: 0 !important; }\n\n /* Entity detail */\n #entity-detail { display: none; }\n .detail-header {\n display: flex; align-items: center; gap: 10px; margin-bottom: 12px;\n }\n .detail-back {\n font-size: 13px; color: var(--sc-accent, #6366f1); cursor: pointer;\n background: none; border: none; padding: 4px 8px;\n }\n .detail-back:hover { text-decoration: underline; }\n #detail-name {\n font-size: 18px; font-weight: 600; color: var(--sc-text, #eee);\n }\n .relation-list { display: flex; flex-direction: column; gap: 4px; }\n .relation-row {\n display: flex; align-items: center; gap: 8px; padding: 8px 12px;\n border-radius: var(--sc-radius-md, 8px); border: 1px solid var(--sc-border, #333);\n background: var(--sc-surface, #1a1a1a); font-size: 13px;\n }\n .rel-kind {\n color: var(--sc-accent, #6366f1); font-weight: 500; min-width: 80px;\n }\n .rel-arrow { color: var(--sc-text-muted, #888); }\n .rel-target { color: var(--sc-text, #eee); flex: 1; }
1\n .rel-source { color: var(--sc-text-muted, #888); font-size: 12px; }\n .rel-edit, .rel-delete {\n background: none; border: none; color: var(--sc-text-muted, #555);\n cursor: pointer; font-size: 14px; padding: 2px 6px; transition: color 0.15s;\n }\n .rel-edit:hover { color: var(--sc-accent, #6366f1); }\n .rel-delete:hover { color: var(--sc-danger, #ef4444); }\n\n /* Search results */\n #search-results { display: none; }\n .search-section-title {\n font-size: 12px; font-weight: 600; color: var(--sc-text-muted, #888);\n text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 6px;\n }\n .search-entity {\n padding: 8px 12px; border-radius: 6px; background: var(--sc-surface, #1a1a1a);\n border: 1px solid var(--sc-border, #333); margin-bottom: 4px;\n display: flex; justify-content: space-between; font-size: 13px;\n cursor: pointer; transition: border-color 0.15s;\n }\n .search-entity:hover { border-color: var(--sc-accent, #6366f1); }\n .search-distance { color: var(--sc-text-muted, #888); font-size: 12px; }\n .search-relation {\n padding: 8px 12px; border-radius: 6px; background: var(--sc-surface, #1a1a1a);\n border: 1px solid var(--sc-border, #333); margin-bottom: 4px; font-size: 13px;\n }\n\n /* Empty state */\n #empty {\n display: none; text-align: center; padding: 40px 20px;\n color: var(--sc-text-muted, #888); font-size: 14px;\n }\n\n /* Spinner */\n #spinner {\n display: none; text-align: center; padding: 20px;\n color: var(--sc-text-muted, #888); font-size: 13px;\n }\n\n /* Status + error bars */\n #status-bar {\n font-size: 12px; color: var(--sc-text-muted, #888);\n text-align: center; padding-top: 4px;\n }\n #error-bar {\n padding: 8px 12px; border-radius: var(--sc-radius-md, 8px);\n background: color-mix(in srgb, var(--sc-danger, #ef4444) 12%, var(--sc-surface, #1a1a1a));\n color: var(--sc-danger, #ef4444); font-size: 13px; display: none;\n }\n</style>\n\n<div id="toolbar">\n <input type="text" id="search-input" placeholder="Search knowledge graph..." />\n <button class="btn" onclick="doSearch()">Search</button>\n <button class="btn btn-muted" onclick="callFn(\'open_add_form\')">+ Add</button>\n</div>\n\n<div id="add-form">\n <div id="form-title" style="font-size:13px;font-weight:600;color:var(--sc-text,#eee);margin-bottom:8px">Add Triple</div>\n <div class="form-row"><label>Subject</label><input type="text" id="f-subject" /></div>\n <div class="form-row"><label>Relation</label><input type="text" id="f-relation" /></div>\n <div class="form-row"><label>Object</label><input type="text" id="f-object" /></div>\n <div class="form-actions">\n <button class="btn btn-sm btn-muted" onclick="SmartChats.app.setState({add_form:null})">Cancel</button>\n <button class="btn btn-sm" onclick="doAddTriple()">Save</button>\n </div>\n</div>\n\n<div id="content">\n <div id="spinner">Loading...</div>\n <div id="empty">No entities in knowledge graph yet.<br>Add some facts to get started.</div>\n <div id="entity-list"></div>\n <div id="entity-detail">\n <div class="detail-header">\n <button class="detail-back" onclick="callFn(\'back_to_list\')">← Back</button>\n <span id="detail-name"></span>\n <button class="btn btn-sm btn-danger" style="margin-left:auto" onclick="if(confirm(\'Delete this entity and all its relations?\')) callFn(\'delete_entity\', {entity: SmartChats.app.state.selected})">Delete Entity</button>\n </div>\n <div id="graph-container"></div>\n <div class="relation-list" id="relation-list"></div>\n </div>\n <div id="search-results">\n <div class="detail-header">\n <button class="detail-back" onclick="callFn(\'back_to_list\')">← Back</button>\n <span id="search-title" style="font-size:14px;color:var(--sc-text-muted,#888)"></span>\n </div>\n <div id="search-body"></div>\n </div>\n</div>\n\n<div id="status-bar"></div>\n<div id="error-bar"></div>\n\n<script>'.concat((0,B.t)(),"</script>\n\n<script>\n // Helper: call an app function with proper (fnArgs, app, util) signature\n function callFn(name, args) {\n return SmartChats.app.fns[name](args || {}, SmartChats.app, SmartChats.util);\n }\n\n function doSearch() {\n var q = document.getElementById('search-input').value.trim();\n if (q) callFn('search', { query: q });\n }\n document.getElementById('search-input').addEventListener('keydown', function(e) {\n if (e.key === 'Enter') doSearch();\n });\n\n function doAddTriple() {\n var s = document.getElementById('f-subject').value.trim();\n var r = document.getElementById('f-relation').value.trim();\n var o = document.getElementById('f-object').value.trim();\n if (s && r && o) callFn('add_triple', { subject: s, relation: r, object: o });\n }\n\n function renderEntityList() {\n var list = document.getElementById('entity-list');\n var entities = SmartChats.app.state.entities || [];\n var view = SmartChats.app.state.view || 'list';\n\n list.style.display = view === 'list' ? 'flex' : 'none';\n document.getElementById('entity-detail').style.display = view === 'detail' ? 'block' : 'none';\n document.getElementById('search-results').style.display = view === 'search' ? 'block' : 'none';\n\n if (view !== 'list') return;\n\n var empty = document.getElementById('empty');\n if (entities.length === 0) {\n list.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n list.innerHTML = entities.map(function(e) {\n return '<div class=\"entity-row\" onclick=\"callFn(\\'select_entity\\',{entity:\\'' + e.name + '\\'})\">' +\n '<span class=\"entity-name\">' + e.name + '</span>' +\n '<span><span class=\"entity-count\">' + (e.relation_count || 0) + ' relations</span> ' +\n '<span class=\"entity-arrow\">\\u203A</span></span></div>';\n }).join('');\n }\n\n function renderDetail() {\n if (SmartChats.app.state.view !== 'detail') return;\n var detail = SmartChats.app.state.detail;\n if (!detail) return;\n\n document.getElementById('detail-name').textContent = detail.name;\n var rlist = document.getElementById('relation-list');\n var relations = detail.relations || [];\n\n rlist.innerHTML = relations.map(function(r) {\n var isSource = r.sourceName === detail.name;\n var other = isSource ? r.targetName : r.sourceName;\n var direction = isSource ? '\\u2192' : '\\u2190';\n return '<div class=\"relation-row\">' +\n '<span class=\"rel-kind\">' + r.kind + '</span>' +\n '<span class=\"rel-arrow\">' + direction + '</span>' +\n '<span class=\"rel-target\">' + other + '</span>' +\n '<button class=\"rel-edit\" onclick=\"callFn(\\'edit_triple\\',{source:\\'' + r.sourceName + '\\',relation:\\'' + r.kind + '\\',target:\\'' + r.targetName + '\\'})\">\\u270E</button>' +\n '<button class=\"rel-delete\" onclick=\"callFn(\\'delete_triple\\',{source:\\'' + r.sourceName + '\\',relation:\\'' + r.kind + '\\',target:\\'' + r.targetName + '\\'})\">\\u00D7</button>' +\n '</div>';\n }).join('');\n }
1\n\n function renderSearch() {\n if (SmartChats.app.state.view !== 'search') return;\n var results = SmartChats.app.state.search_results;\n if (!results) return;\n\n document.getElementById('search-title').textContent = 'Search: \"' + results.query + '\"';\n var body = document.getElementById('search-body');\n var html = '';\n\n if (results.entities && results.entities.length > 0) {\n html += '<div class=\"search-section-title\">Entities</div>';\n html += results.entities.map(function(e) {\n return '<div class=\"search-entity\" onclick=\"callFn(\\'select_entity\\',{entity:\\'' + e.name + '\\'})\">' +\n '<span>' + e.name + '</span>' +\n '<span class=\"search-distance\">' + (e.distance != null ? e.distance.toFixed(3) : '') + '</span></div>';\n }).join('');\n }\n\n if (results.relations && results.relations.length > 0) {\n html += '<div class=\"search-section-title\">Relations</div>';\n html += results.relations.map(function(r) {\n return '<div class=\"search-relation\">' +\n '<span>' + r.sourceName + '</span>' +\n ' <span style=\"color:var(--sc-accent,#6366f1)\">[' + r.kind + ']</span> ' +\n '<span>' + r.targetName + '</span>' +\n (r.distance != null ? ' <span class=\"search-distance\">(' + r.distance.toFixed(3) + ')</span>' : '') +\n '</div>';\n }).join('');\n }\n\n if (!html) html = '<div style=\"padding:20px;text-align:center;color:var(--sc-text-muted)\">No results found</div>';\n body.innerHTML = html;\n }
1\n\n function renderStatus() {\n var entities = SmartChats.app.state.entities || [];\n var total = 0;\n entities.forEach(function(e) { total += e.relation_count || 0; });\n // Relations are double-counted (each relation touches two entities)\n var relCount = Math.round(total / 2) || total;\n document.getElementById('status-bar').textContent =\n entities.length + ' entities' + (relCount > 0 ? ' \\u00B7 ' + relCount + ' relations' : '');\n }\n\n function renderAddForm() {\n var form = document.getElementById('add-form');\n var data = SmartChats.app.state.add_form;\n form.style.display = data ? 'block' : 'none';\n if (data) {\n document.getElementById('form-title').textContent = data.editing ? 'Edit Triple' : 'Add Triple';\n document.getElementById('f-subject').value = data.subject || '';\n document.getElementById('f-relation').value = data.relation || '';\n document.getElementById('f-object').value = data.object || '';\n }\n }\n\n function renderLoading() {\n var spinner = document.getElementById('spinner');\n spinner.style.display = SmartChats.app.state.loading ? 'block' : 'none';\n }\n\n function renderError() {\n var bar = document.getElementById('error-bar');\n var err = SmartChats.app.state.error || '';\n if (err) { bar.textContent = err; bar.style.display = 'block'; }\n else { bar.textContent = ''; bar.style.display = 'none'; }\n }\n\n // ââ Graph Visualization (star_graph lib injected via getStarGraphSource) ââ\n var starGraph = null;\n\n function renderGraph() {\n var container = document.getElementById('graph-container');\n var view = SmartChats.app.state.view;\n\n if (view !== 'detail' || !SmartChats.app.state.detail) {\n container.style.display = 'none';\n if (starGraph) { starGraph.destroy(); starGraph = null; }\n return;\n }\n\n if (typeof graphology === 'undefined' || typeof Sigma === 'undefined' || typeof createStarGraph === 'undefined') {\n container.style.display = 'none';\n return;\n }\n\n var detail = SmartChats.app.state.detail;\n var relations = detail.relations || [];\n if (relations.length === 0) { container.style.display = 'none'; return; }\n\n container.style.display = 'block';\n\n if (starGraph) { starGraph.destroy(); starGraph = null; }\n\n // Read theme from CSS vars\n var s = getComputedStyle(document.documentElement);\n var accent = s.getPropertyValue('--sc-accent').trim() || '#6366f1';\n var bg = s.getPropertyValue('--sc-background').trim() || '#0d1117';\n var border = s.getPropertyValue('--sc-border').trim() || '#333';\n var textMuted = s.getPropertyValue('--sc-text-muted').trim() || '#888';\n\n // Parse hex to RGB for particle blending\n function hexToRGB(hex) {\n var h = hex.replace('#', '');\n if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];\n return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];\n }\n var accentRGB = hexToRGB(accent);\n var bgRGB = hexToRGB(bg);\n\n // Dim version of accent for node fill â blend 85% toward background (sigma ignores alpha)\n function blendHex(fg, bg, t) {\n return fg.map(function(f, i) { return Math.floor(f * t + bg[i] * (1 - t)); });\n }\n function rgbToHex(rgb) {\n return '#' + rgb.map(function(c) { return (c < 16 ? '0' : '') + c.toString(16); }).join('');\n }\n var accentDim = rgbToHex(blendHex(accentRGB, bgRGB, 0.15));\n var innerColor = rgbToHex(blendHex(accentRGB, bgRGB, 0.25));\n\n starGraph = createStarGraph({\n graphology: graphology, Sigma: Sigma, container: container,\n theme: {\n accent: accent, accentDim: accentDim, innerColor: innerColor,\n particleRGB: accentRGB, bgRGB: bgRGB,\n edgeColor: border, edgeLabelColor: textMuted,\n labelColor: accent, expandableColor: textMuted,\n },\n rootId: detail.name, rootLabel: detail.name,\n onNodeClick: function(nodeId) {\n // Navigate to clicked entity\n callFn('select_entity', { entity: nodeId });\n },\n });\n\n // Build children from current relations\n // For outgoing (centerâother): add as normal child\n // For incoming (otherâcenter): add other as parent of center (flipped)\n var outgoing = [];\n var incoming = [];\n var seen = {};\n for (var i = 0; i < relations.length;
1 i++) {\n var r = relations[i];\n var isSource = r.sourceName === detail.name;\n var other = isSource ? r.targetName : r.sourceName;\n if (other !== detail.name && !seen[other]) {\n seen[other] = true;\n if (isSource) {\n outgoing.push({ name: other, kind: r.kind });\n } else {\n incoming.push({ name: other, kind: r.kind });\n }\n }\n }\n // All relations as children â outgoing edges are centerâchild, incoming are childâcenter\n // For incoming, we swap: add the other node, then the edge goes otherâcenter\n var allChildren = outgoing.concat(incoming.map(function(r) { return { name: r.name, kind: r.kind, reverse: true }; }));\n starGraph.expandNode(detail.name, allChildren);\n }\n\n SmartChats.app.onRender(function(state, changed) {\n if (changed.has('entities') || changed.has('view')) { renderEntityList(); renderStatus(); }\n if (changed.has('detail') || changed.has('view') || changed.has('selected')) { renderEntityList(); renderDetail(); renderGraph(); }\n if (changed.has('search_results') || changed.has('view')) { renderEntityList(); renderSearch(); }\n if (changed.has('add_form')) renderAddForm();\n if (changed.has('loading')) renderLoading();\n if (changed.has('error')) renderError();\n });\n\n renderEntityList();\n renderStatus();\n renderAddForm();\n renderLoading();\n renderError();\n</script>\n"),j=[w,x,E,C,I,A,{id:"kg_explorer",name:"Knowledge Graph Explorer",version:"1.1.0",description:"Browse, search, add, and delete knowledge graph facts. See what SmartChats knows about you and manage your stored knowledge.",icon:"\uD83D\uDD17",source:"builtin",categories:["data","knowledge"],tags:["knowledge_graph","explorer","builtin"],interaction_mode:"hybrid",display_mode:"panel",permissions:[...h.builtin],external_scripts:["/lib/graphology.umd.min.js","/lib/sigma.min.js"],requested_functions:["store_declarative_knowledge","retrieve_declarative_knowledge","delete_declarative_knowledge","get_knowledge_graph_entities","get_entity_detail"],html_templates:{main:R},state_schema:{entities:{type:"array",default:[],persist:!1,description:"List of entities with relation counts"},selected:{type:"string",default:null,persist:!1,description:"Currently selected entity name"},detail:{type:"object",default:null,persist:!1,description:"Selected entity detail {name, relations}"},search_query:{type:"string",default:"",persist:!1,description:"Current search query"},search_results:{type:"object",default:null,persist:!1,description:"Search results {entities, relations}"},view:{type:"string",default:"list",persist:!1,description:"Current view: list | detail | search"},add_form:{type:"object",default:null,persist:!1,description:"Add triple form state"},loading:{type:"boolean",default:!1,persist:!1,description:"Loading indicator"},error:{type:"string",default:"",persist:!1,description:"Error message"}},on_activate:"on_activate",voice_hooks:{wants_transcripts:!1},modules:[{id:"main",name:"KG Explorer",position:55,system_msg:"The Knowledge Graph Explorer app is active. It lets you browse, search, add, and delete knowledge graph facts.\n\nAvailable functions:\n- kg_explorer_load_entities: Refresh the entity list\n- kg_explorer_select_entity: View an entity's relationships\n- kg_explorer_back_to_list: Return to entity list\n- kg_explorer_search: Semantic search the knowledge graph\n- kg_explorer_add_triple: Add a new fact (subject, relation, object)\n- kg_explorer_delete_triple: Delete a specific relationship\n- kg_explorer_delete_entity: Delete an entity and all its relationships\n- kg_explorer_open_add_form: Show the add triple form",functions:[{name:"on_activate",description:"Initialize KG Explorer â loads entity list.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ loading: true });\n try {\n var entities = await util.smartchats.get_knowledge_graph_entities({});\n app.setState({ entities: entities || [], loading: false, view: 'list' });\n } catch(e) {\n app.setState({ loading: false, error: 'Failed to load entities: ' + (e.message || e) });\n }\n return { activated: true };\n}"},{name:"load_entities",description:"Refresh the entity list.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ loading: true, error: '' });\n try {\n var entities = await util.smartchats.get_knowledge_graph_entities({});\n app.setState({ entities: entities || [], loading: false });\n return { ok: true, count: (entities || []).length };\n } catch(e) {\n app.setState({ loading: false, error: 'Failed to load: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"select_entity",description:"Select an entity to view its relationships.",parameters:{entity:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var name = fnArgs.entity;\n if (!name) return { ok: false, error: 'entity required' };\n app.setState({ loading: true, error: '' });\n try {\n var detail = await util.smartchats.get_entity_detail({ entity: name });\n app.setState({\n selected: detail.name,\n detail: detail,\n view: 'detail',\n loading: false,\n });\n return { ok: true, name: detail.name, relations: detail.relations.length };\n } catch(e) {\n app.setState({ loading: false, error: 'Failed to load entity: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"back_to_list",description:"Return to entity list view.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ selected: null, detail: null, search_results: null, view: 'list', add_form: null });\n return { ok: true };\n}"},{name:"search",description:"Semantic search the knowledge graph.",parameters:{query:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var query = fnArgs.query || app.state.search_query;\n if (!query) return { ok: false, error: 'query required' };\n app.setState({ loading: true, search_query: query, error: '' });\n try {\n var formatted = await util.smartchats.retrieve_declarative_knowledge({ query: query, limit: 20 });\n // Parse the formatted text back into structured data\n var entities = [];\n var relations = [];\n var section = '';\n var lines = (formatted || '').split('\\n');\n for (var i = 0; i < lines.length;
1 i++) {\n var line = lines[i].trim();\n if (line === 'Entities:') { section = 'e'; continue; }\n if (line === 'Relations:') { section = 'r'; continue; }\n if (line.startsWith('- ') && section === 'e') {\n var m = line.match(/^- (.+?)\\s*\\(distance: ([\\d.]+)\\)/);\n if (m) entities.push({ name: m[1], distance: parseFloat(m[2]) });\n }\n if (line.startsWith('- ') && section === 'r') {\n var m2 = line.match(/^- (.+?) --\\[(.+?)\\]--> (.+?)(?:\\s*\\(distance: ([\\d.]+)\\))?$/);\n if (m2) relations.push({ sourceName: m2[1], kind: m2[2], targetName: m2[3], distance: m2[4] ? parseFloat(m2[4]) : null });\n }\n }\n var results = { query: query, entities: entities, relations: relations };\n app.setState({ search_results: results, view: 'search', loading: false });\n return { ok: true, entities: entities.length, relations: relations.length };\n } catch(e) {\n app.setState({ loading: false, error: 'Search failed: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"add_triple",description:"Add a new knowledge triple.",parameters:{subject:"string",relation:"string",object:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var subject = fnArgs.subject;\n var relation = fnArgs.relation;\n var object = fnArgs.object;\n if (!subject || !relation || !object) return { ok: false, error: 'subject, relation, and object required' };\n app.setState({ loading: true, error: '' });\n try {\n // If editing, delete the old triple first\n var editing = app.state.add_form && app.state.add_form.editing;\n if (editing) {\n await util.smartchats.delete_declarative_knowledge({ triples: [[editing.source, editing.relation, editing.target]] });\n }\n await util.smartchats.store_declarative_knowledge({ triples: [[subject, relation, object]] });\n app.setState({ add_form: null, loading: false });\n // Reload current view\n if (app.state.view === 'detail' && app.state.selected) {\n var detail = await util.smartchats.get_entity_detail({ entity: app.state.selected });\n app.setState({ detail: detail });\n } else {\n var entities = await util.smartchats.get_knowledge_graph_entities({});\n app.setState({ entities: entities || [] });\n }\n return { ok: true };\n } catch(e) {\n app.setState({ loading: false, error: 'Failed to add: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"delete_triple",description:"Delete a specific relationship.",parameters:{source:"string",relation:"string",target:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var source = fnArgs.source;\n var relation = fnArgs.relation;\n var target = fnArgs.target;\n if (!source || !relation || !target) return { ok: false, error: 'source, relation, and target required' };\n app.setState({ loading: true, error: '' });\n try {\n await util.smartchats.delete_declarative_knowledge({ triples: [[source, relation, target]] });\n // Reload detail view\n if (app.state.selected) {\n var detail = await util.smartchats.get_entity_detail({ entity: app.state.selected });\n app.setState({ detail: detail, loading: false });\n } else {\n app.setState({ loading: false });\n }\n return { ok: true };\n } catch(e) {\n app.setState({ loading: false, error: 'Delete failed: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"delete_entity",description:"Delete an entity and all its relationships.",parameters:{entity:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var entity = fnArgs.entity || app.state.selected;\n if (!entity) return { ok: false, error: 'entity required' };\n app.setState({ loading: true, error: '' });\n try {\n var result = await util.smartchats.delete_declarative_knowledge({ entity: entity });\n var entities = await util.smartchats.get_knowledge_graph_entities({});\n app.setState({ entities: entities || [], selected: null, detail: null, view: 'list', loading: false });\n return { ok: true, relations_deleted: result.relations_deleted || 0 };\n } catch(e) {\n app.setState({ loading: false, error: 'Delete failed: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"},{name:"edit_triple",description:"Edit a triple â opens form pre-filled with existing values.",parameters:{source:"string",relation:"string",target:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n var source = fnArgs.source;\n var relation = fnArgs.relation;\n var target = fnArgs.target;\n if (!source || !relation || !target) return { ok: false, error: 'source, relation, and target required' };\n app.setState({ add_form: { subject: source, relation: relation, object: target, editing: { source: source, relation: relation, target: target } } });\n return { ok: true };\n}"},{name:"open_add_form",description:"Show the add triple form.",parameters:{subject:"string",relation:"string",object:"string"},return_type:"object",code:"async function(fnArgs, app, util) {\n app.setState({ add_form: { subject: fnArgs.subject || '', relation: fnArgs.relation || '', object: fnArgs.object || '', editing: null } });\n return { ok: true };\n}"},{name:"dom_check",description:"Check DOM state against app state.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var view = app.state.view || 'list';\n var entityRows = app.dom.querySelectorAll('.entity-row').length;\n var entitiesInState = (app.state.entities || []).length;\n\n var detailEl = app.dom.getElementById('entity-detail');\n var detailNameEl = app.dom.getElementById('detail-name');\n var detailEntity = detailNameEl ? detailNameEl.textContent : null;\n var relationRows = app.dom.querySelectorAll('.relation-row').length;\n var detailRelationsInState = app.state.detail ? (app.state.detail.relations || []).length : 0;\n\n var searchEl = app.dom.getElementById('search-results');\n var searchVisible = searchEl ? searchEl.style.display !== 'none' : false;\n\n var addForm = app.dom.getElementById('add-form');\n var addFormVisible = addForm ? addForm.style.display !== 'none' : false;\n\n var emptyEl = app.dom.getElementById('empty');\n var emptyVisible = emptyEl ? emptyEl.style.display !== 'none' : false;\n\n var statusEl = app.dom.getElementById('status-bar');\n var statusText = statusEl ? statusEl.textContent : '';\n\n var graphEl = app.dom.getElementById('graph-container');\n var graphVisible = graphEl ? graphEl.style.display !== 'none' : false;\n\n return {\n view: view,\n entity_count: entityRows,\n entities_in_state: entitiesInState,\n entities_match: entityRows === entitiesInState,\n detail_entity: detailEntity,\n detail_relations: relationRows,\n detail_match: relationRows === detailRelationsInState,\n search_visible: searchVisible,\n add_form_visible: addFormVisible,\n empty_visible: emptyVisible,\n loading: app.state.loading || false,\n status_text: statusText,\n graph_visible: graphVisible,\n };\n}"},{name:"seed_test_data",description:"Store test triples for testing.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var triples = [\n ['__simi_alice', 'knows', '__simi_bob'],\n ['__simi_alice', 'works_at', '__simi_acme'],\n ['__simi_bob', 'lives_in', '__simi_paris'],\n ['__simi_alice', 'interested_in', '__simi_cooking'],\n ['__simi_acme', 'located_in', '__simi_nyc'],\n ];\n app.setState({ loading: true, error: '' });\n try {\n await util.smartchats.store_declarative_knowledge({ triples: triples });\n app.setState({ loading: false });\n return { seeded: true, count: triples.length };\n } catch(e) {\n app.setState({ loading: false, error: 'Seed failed: ' + (e.message || e) });
1\n return { seeded: false, error: e.message || String(e) };\n }\n}"},{name:"cleanup_test_data",description:"Delete test entities.",parameters:null,return_type:"object",code:"async function(fnArgs, app, util) {\n var testEntities = ['__simi_alice', '__simi_bob', '__simi_acme', '__simi_paris', '__simi_cooking', '__simi_nyc', '__simi_jazz'];\n app.setState({ loading: true, error: '' });\n try {\n for (var i = 0; i < testEntities.length; i++) {\n try { await util.smartchats.delete_declarative_knowledge({ entity: testEntities[i] }); } catch(e) { /* ignore */ }\n }\n var entities = await util.smartchats.get_knowledge_graph_entities({});\n app.setState({ entities: entities || [], loading: false });\n return { ok: true, cleaned: testEntities.length };\n } catch(e) {\n app.setState({ loading: false, error: 'Cleanup failed: ' + (e.message || e) });\n return { ok: false, error: e.message || String(e) };\n }\n}"}]}],version_history:[{version:"1.0.0",published_at:new Date().toISOString()}]}],T="[app-seeder]";async function D(e){let t=[],n=[],a=[];for(let r of j)try{let i=await o(r.id),c=function(e){let t=JSON.stringify({name:e.name,description:e.description,modules:e.modules,html_templates:e.html_templates,state_schema:e.state_schema,permissions:e.permissions,requested_functions:e.requested_functions,interaction_mode:e.interaction_mode,display_mode:e.display_mode,on_activate:e.on_activate,on_deactivate:e.on_deactivate,voice_hooks:e.voice_hooks,version:e.version,external_scripts:e.external_scripts}),n=5381;for(let e=0;e<t.length;e++)n=(n<<5)+n+t.charCodeAt(e)&4294967295;return(n>>>0).toString(36)}(r);if(i){let t=i._content_hash||"";if(t===c){console.log("".concat(T," ").concat(r.id," v").concat(r.version," â skipped (hash match: ").concat(c,")")),a.push(r.id);continue}console.log("".concat(T," ").concat(r.id," v").concat(r.version," â updating (hash ").concat(t||"none"," â ").concat(c,")"));let s="".concat(r.name," ").concat(r.description),o=await e(s);await l(r.id,{...r,_content_hash:c,published_at:new Date().toISOString()},o),await u(r.id,{installed_version:r.version}),n.push(r.id)}else{console.log("".concat(T," ").concat(r.id," v").concat(r.version," â seeding new (hash: ").concat(c,")"));let n="".concat(r.name," ").concat(r.description),a=await e(n);await s({...r,_content_hash:c,published_at:new Date().toISOString()},a),await p(r.id)||await d({app_id:r.id,installed_version:r.version,granted_permissions:h.builtin,app_state:{},config:{},activation_count:0}),t.push(r.id)}}catch(t){let e=t instanceof Error?t.message:String(t);if(e.includes("Database index")&&e.includes("already contains")){console.log("".concat(T," ").concat(r.id," â already seeded (concurrent writer); skipping")),a.push(r.id);continue}console.error("".concat(T," ").concat(r.id," â FAILED:"),t)}return console.log("".concat(T," done: seeded=").concat(t.length," updated=").concat(n.length," skipped=").concat(a.length)),{seeded:t,updated:n,skipped:a}}var q=n(8114);let M=null,z=null,L=[],N=null;async function F(){try{await D(a.D).catch(()=>null);let e=await g(),t=(await Promise.all(e.map(async e=>({install:e,manifest:await o(e.app_id).catch(()=>null)})))).filter(e=>null!==e.manifest);L=t.map(e=>e.install);let n={};for(let e of t)n[e.install.app_id]=e.manifest;q.useSmartChatsStore.setState({installedApps:t.map(e=>e.install),appManifestCache:n});let r=q.useSmartChatsStore.getState().agent;if(r){let e=t.map(e=>({id:e.manifest.id,name:e.manifest.name,description:e.manifest.description,icon:e.manifest.icon}));try{r.add_user_data_input({installed_apps:e},"bg_installed_apps")}catch(e){}}return t.map(e=>e.install)}catch(e){return[]}}function H(){return N||(N=F()),N}function P(){N=null}function O(){return z}function U(){var e;return null!==(e=null==z?void 0:z.isInputRequested())&&void 0!==e&&e}function G(e,t){async function r(a){if(!M||!z)return;let r=M.manifest.id;if(!0!==M.preview){let e=a.util.get_workspace()||{},t={};try{let{useSmartChatsStore:e}=n(8114);t=e.getState().workspace||{}}catch(e){}let i={...e,...t},s=r+".",o=M.manifest.state_schema||{},l={};for(let[e,t]of Object.entries(i))if(e.startsWith(s)){let n=e.slice(s.length),a=o[n];a&&!1===a.persist||(l[n]=t)}await u(r,{app_state:l}).catch(()=>{})}z.destroy(),z=null,e.remove_module("app_".concat(r,"_context")),t(),M=null,a.util.event({type:"app_deactivated",app_id:r})}async function i(n,a,i,s){let o=n.id,l=(null==s?void 0:s.preview)===!0;M&&await r(i);let c=new b(n,a,{onUtilCall:async(e,t)=>await w(e,t,n,i),onLog:e=>i.util.log(e),onFeedback:e=>{let t=i.util.sounds;t&&t[e]&&t[e]()}});z=c;let d=function(e,t){let n=[];for(let a of e.modules)for(let r of a.functions||[])n.push({enabled:!0,name:"".concat(e.id,"_").concat(r.name),description:r.description,parameters:r.parameters,return_type:r.return_type||"any",fn:async e=>await t.callFunction(r.name,e.params)});return n}(n,c),p={id:"app_".concat(o,"_context"),name:"App: ".concat(n.name),position:60,system_msg:function(e){var t,n;let a=e.interaction_mode||"agent_driven",r=null===(n=e.modules)||void 0===n?void 0:null===(t=n[0])||void 0===t?void 0:t.system_msg;if(r)return r;let i="[Active App: ".concat(e.name,"]\n").concat(e.description,"\n");if("app_driven"===a?i+="\nThis app manages its own interaction loop. It will handle user input directly.":i+="\nYou have access to this app's functions (prefixed with \"".concat(e.id,'_"). Use them to interact with the app on behalf of the user.'),e.state_schema){let t=Object.keys(e.state_schema);i+="\nApp state keys: ".concat(t.join(", "))+"\nAccess via ".concat(e.id,"_get_app_state / ").concat(e.id,"_set_app_state or the dedicated app functions.")}return i}(n),functions:d};if(e.add_module(p),t(),n.state_schema){let e={};for(let[t,a]of Object.entries(n.state_schema)){let n="".concat(o,".").concat(t);n in i.util.get_workspace()||(e[n]=a.default)}Object.keys(e).length>0&&i.util.update_workspace(e)}if(!l&&a.app_state&&Object.keys(a.app_state).length>0){let e=n.state_schema||{},t={};for(let[n,r]of Object.entries(a.app_state)){let a=e[n];a&&!1===a.persist||(t["".concat(o,".").concat(n)]=r)}i.util.update_workspace(t)}return l||await u(o,{last_activated_at:new Date().toISOString(),activation_count:(a.activation_count||0)+1}
1).catch(()=>{}),M={manifest:n,install:a,state:"active",preview:l},i.util.event({type:"app_activated",manifest:n,install:a,sandbox:c}),'App "'.concat(n.name,'" activated').concat(l?" (preview)":"")}async function v(e){let t=await g(),n=t.map(e=>e.app_id);if(n.includes(e))return e;let a=e.toLowerCase().trim().replace(/[\s-]+/g,"_");if(n.includes(a))return a;for(let n of t){let t=await o(n.app_id).catch(()=>null);if((null==t?void 0:t.name)&&t.name.toLowerCase()===e.toLowerCase())return n.app_id}return e}async function y(e,t){let n=await v(e),a=await p(n);if(!a){let t=(await g()).map(e=>e.app_id).join(", ")||"none";throw Error("App '".concat(e,"' is not installed. Installed: ").concat(t))}let r=await o(n);if(!r)throw Error("App '".concat(n,"' not found in registry"));return await i(r,a,t)}function _(e){let{name:t,description:n,html:a,functions:r,state_schema:i,voice_hooks:s,tags:o,categories:l,permissions:c,requested_functions:d,interaction_mode:p}=e;if(!t||!n)throw Error("name and description are required");return{id:t.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,""),name:t,version:"1.0.0",description:n,source:"agent",categories:l||[],tags:o||[],modules:[{id:"main",name:t,position:60,system_msg:n,functions:(r||[]).map(e=>({name:e.name,description:e.description||"",parameters:e.parameters||null,return_type:e.return_type||"any",code:e.code}))}],interaction_mode:p||"agent_driven",html_templates:a?{main:a}:void 0,display_mode:"panel",state_schema:i||void 0,permissions:c||h.agent,requested_functions:d||[],voice_hooks:s||void 0,version_history:[{version:"1.0.0",published_at:new Date().toISOString()}]}}async function w(t,n,r,i){let s=r.id;if("update_workspace"===t){let e={};for(let[t,a]of Object.entries(n))e["".concat(s,".").concat(t)]=a;return i.util.update_workspace(e),{ok:!0}}if("get_workspace"===t){let e=i.util.get_workspace(),t=s+".",n={};for(let[a,r]of Object.entries(e))a.startsWith(t)&&(n[a.slice(t.length)]=r);return n}if("user_output"===t)return i.util.user_output("string"==typeof n?n:(null==n?void 0:n.text)||String(n)),{ok:!0};if("get_user_input"===t)return await i.util.get_user_data();if("call_llm"===t)throw Error("call_llm not yet implemented for apps");if("get_embedding"===t){let e="string"==typeof n?n:(null==n?void 0:n.text)||String(n);return await i.util.get_embedding(e)}if("query"===t){var o,l;let{query:e,variables:t}=n||{},r=await (0,a.getBackend)().data.query({query:e,variables:t});return(null==r?void 0:null===(l=r.data)||void 0===l?void 0:null===(o=l.result)||void 0===o?void 0:o.result)||[]}if(t.startsWith("smartchats.")){let a=t.slice(11),r=e.build().functions.find(e=>e.name===a);if(!r)throw Error("SmartChats function not found: ".concat(a));return await r.fn({params:n||{},util:i.util})}throw Error("Unknown util method: ".concat(t))}return{id:"app_launcher",name:"App Platform",position:6,system_msg:"[App Platform]\nYou can manage mini-apps that extend your capabilities.\nUse the app management functions to create, install, activate, or search for apps.\nTo iterate on an app without installing: preview_app â modify workspace.__preview_app â update_preview â repeat â save_preview when done.",state:"",beforeBuild(){let e=M?"".concat(M.manifest.name).concat(M.preview?" (preview)":""):"none",t=L.length>0?L.map(e=>e.app_id).join(", "):"none";this.state="active_app: ".concat(e,"\ninstalled: ").concat(t)},functions:[{enabled:!0,description:"List all installed apps with their summaries.",name:"list_apps",return_shape:"Array of installed app summaries: [{ app_id: string, name: string, description: string, version: string, activation_count: number, is_active: boolean }]. is_active=true marks the currently activated app (only one at a time).",parameters:null,fn:async e=>{let{log:t}=e.util;t("Listing installed apps");let n=await g();L=n;let a=[];for(let e of n){let t=await o(e.app_id).catch(()=>null);a.push({app_id:e.app_id,name:(null==t?void 0:t.name)||e.app_id,description:(null==t?void 0:t.description)||"",version:e.installed_version,activation_count:e.activation_count,is_active:(null==M?void 0:M.manifest.id)===e.app_id})}return a},return_type:"array"},{enabled:!0,description:"Search for apps in the registry by natural language query. Returns matching apps with relevance scores.",name:"search_apps",return_shape:"Array of matched apps with semantic-similarity scores: [{ id: string, name: string, description: string, icon: string, source: string, score: number }]. Sorted by score DESC (highest match first).",parameters:{query:"string"},fn:async e=>{let{query:t}=e.params,{log:n,get_embedding:a}=e.util;n("Searching apps: ".concat(t));let r=await a(t);return(await c(r,10)).map(e=>({id:e.id,name:e.name,description:e.description,icon:e.icon,source:e.source,score:e.score}))},return_type:"array"},{enabled:!0,description:"Activate an installed app. Loads its UI, functions, and context into the agent. Only one app can be active at a time.",name:"activate_app",parameters:{app_id:"string"},fn:async e=>{let{app_id:t}=e.params,{log:n}=e.util;return n("Activating app: ".concat(t)),await y(t,e)},return_type:"string"},{enabled:!0,description:"Deactivate the currently active app. Persists its state and removes it from the agent context.",name:"deactivate_app",parameters:null,fn:async e=>{let{log:t}=e.util;if(!M)return"No app is currently active";let n=M.manifest.name;return t("Deactivating app: ".concat(n)),await r(e),'App "'.concat(n,'" deactivated')},return_type:"string"},{enabled:!0,description:"Install an app from the registry. Does not activate it.",n
1ame:"install_app",parameters:{app_id:"string"},fn:async e=>{let{app_id:t}=e.params,{log:n}=e.util;n("Installing app: ".concat(t));let a=await o(t);if(!a)throw Error("App '".concat(t,"' not found in registry"));if(await p(t))return"App '".concat(t,"' is already installed");let r=h[a.source]||h.community,i=await d({app_id:t,installed_version:a.version,granted_permissions:r,app_state:{},config:{},activation_count:0});return await f(t).catch(()=>{}),L.push(i),e.util.event({type:"app_installed",manifest:a,install:i}),'App "'.concat(a.name,'" installed')},return_type:"string"},{enabled:!0,description:"Uninstall an app. Deactivates it first if active, then removes the install record.",name:"uninstall_app",parameters:{app_id:"string"},fn:async e=>{let{app_id:t}=e.params,{log:n}=e.util;return n("Uninstalling app: ".concat(t)),(null==M?void 0:M.manifest.id)===t&&await r(e),await m(t),L=L.filter(e=>e.app_id!==t),e.util.event({type:"app_uninstalled",app_id:t}),"App '".concat(t,"' uninstalled")},return_type:"string"},{enabled:!0,description:"Create a new mini-app, save to registry, install, and activate. Function signature: (fnArgs, app, util). app = { dom, state, fns, manifest, el(selector) }. util = { smartchats, user_output, get_user_input, update_workspace, get_workspace, log, feedback }. The html parameter is the app's iframe HTML template.",name:"create_app",return_shape:"{ app_id: string, name: string, message: string }. Use app_id for any subsequent activate/update/uninstall calls. message is a user-facing confirmation string.",parameters:{name:"string",description:"string",html:"string",functions:"array",state_schema:"object",voice_hooks:"object",tags:"array",categories:"array",permissions:"array",requested_functions:"array",interaction_mode:"string"},fn:async e=>{let{log:t,get_embedding:n}=e.util,a=_(e.params),r=a.id;t("Creating app: ".concat(a.name," (").concat(r,")"));let i="".concat(a.name," ").concat(a.description),o=await n(i);await s(a,o);let l=h.agent,c=await d({app_id:r,installed_version:"1.0.0",granted_permissions:l,app_state:{},config:{},activation_count:0});return L.push(c),e.util.event({type:"app_installed",manifest:a,install:c}),await y(r,e),{app_id:r,name:a.name,message:'App "'.concat(a.name,'" created and activated')}},return_type:"object"},{enabled:!0,description:"Update an existing app. Can modify HTML, state_schema, description, or functions. Bumps the version and hot-reloads if the app is currently active.",name:"update_app",return_shape:"{ app_id: string, version: string (the new bumped version), message: string }. Throws (no error-object return) if app_id is not found.",parameters:{app_id:"string",html:"string",description:"string",state_schema:"object",functions:"array"},fn:async e=>{let t;let{app_id:n,html:a,description:i,state_schema:s,functions:c}=e.params,{log:d,get_embedding:p}=e.util;d("Updating app: ".concat(n));let u=await o(n);if(!u)throw Error("App '".concat(n,"' not found"));let m={};if(void 0!==a&&(m.html_templates={...u.html_templates,main:a}),void 0!==i&&(m.description=i),void 0!==s&&(m.state_schema=s),void 0!==c){let e=(c||[]).map(e=>({name:e.name,description:e.description||"",parameters:e.parameters||null,return_type:e.return_type||"any",code:e.code}));m.modules=u.modules.map((t,n)=>0===n?{...t,functions:e}:t)}let g=u.version.split(".").map(Number);g[2]=(g[2]||0)+1,m.version=g.join("."),m.version_history=[...u.version_history||[],{version:m.version,published_at:new Date().toISOString()}],i&&(t=await p("".concat(u.name," ").concat(i))),await l(n,m,t);let f={...u,...m};return e.util.event({type:"app_updated",manifest:f}),(null==M?void 0:M.manifest.id)===n&&(d("Hot reloading active app..."),await r(e),await y(n,e)),{app_id:n,version:m.version,message:"App updated to v".concat(m.version)}},return_type:"object"},{enabled:!0,description:"Preview an app without installing it. Use update_preview to iterate and save_preview to promote.",name:"preview_app",parameters:{name:"string",description:"string",html:"string",functions:"array",state_schema:"object",tags:"array",categories:"array",permissions:"array",requested_functions:"array",interaction_mode:"string"},fn:async e=>{let{log:t}=e.util,n=_(e.params);
1t("Preview app: ".concat(n.name," (").concat(n.id,")")),e.util.update_workspace({__preview_app:e.params});let a={app_id:n.id,installed_version:n.version,granted_permissions:h.builtin,app_state:{},config:{},activation_count:0};return await i(n,a,e,{preview:!0}),{app_id:n.id,name:n.name,message:'Preview "'.concat(n.name,'" loaded. Modify workspace.__preview_app and call update_preview to iterate. Call save_preview to install permanently.')}},return_type:"object"},{enabled:!0,description:"Hot-reload the current preview app with an updated definition.",name:"update_preview",parameters:{name:"string",description:"string",html:"string",functions:"array",state_schema:"object",tags:"array",categories:"array",permissions:"array",requested_functions:"array",interaction_mode:"string"},fn:async e=>{let{log:t}=e.util;if(!(null==M?void 0:M.preview))throw Error("No preview app is active. Call preview_app first.");let n=_(e.params);t("Updating preview: ".concat(n.name)),e.util.update_workspace({__preview_app:e.params});let a={app_id:n.id,installed_version:n.version,granted_permissions:h.builtin,app_state:{},config:{},activation_count:0};return await i(n,a,e,{preview:!0}),{app_id:n.id,name:n.name,message:'Preview "'.concat(n.name,'" updated and reloaded.')}},return_type:"object"},{enabled:!0,description:"Promote the current preview app to a permanent installed app. Saves the manifest to the database, creates an install record, and clears the preview flag. The app stays active.",name:"save_preview",parameters:null,fn:async e=>{let{log:t,get_embedding:n}=e.util;if(!(null==M?void 0:M.preview))throw Error("No preview app is active. Nothing to save.");let a=M.manifest,r=a.id;t("Saving preview as installed app: ".concat(a.name," (").concat(r,")"));let i="".concat(a.name," ").concat(a.description),c=await n(i);await o(r).catch(()=>null)?await l(r,{...a,published_at:new Date().toISOString()},c):await s({...a,published_at:new Date().toISOString()},c);let m=await p(r).catch(()=>null),g=h.agent;if(m)await u(r,{installed_version:a.version});else{let e=await d({app_id:r,installed_version:a.version,granted_permissions:g,app_state:{},config:{},activation_count:1});L.push(e)}return M.preview=!1,e.util.update_workspace({__preview_app:null}),e.util.event({type:"app_installed",manifest:a,install:M.install}),{app_id:r,name:a.name,message:'Preview "'.concat(a.name,'" saved as installed app.')}},return_type:"object"},{enabled:!0,description:"Read from the active app's state. Pass a key to read one value, or omit key to get the full state object. Reads directly from the app's iframe.",name:"get_app_state",return_shape:"When called without a key: { [key: string]: any } â the full active-app state object (workspace entries with the app's prefix, stripped). When called with a key: any (the single value at that key, or undefined). Throws if no app is currently active.",parameters:{key:"string"},fn:async e=>{let{key:t}=e.params;if(!M)throw Error("No app is currently active");if(z)try{return await z.callFunction("__get_state",{key:t||null})}catch(e){}if(!t){let t=e.util.get_workspace(),n=M.manifest.id+".",a={};for(let[e,r]of Object.entries(t))e.startsWith(n)&&(a[e.slice(n.length)]=r);return a}return e.util.get_workspace()["".concat(M.manifest.id,".").concat(t)]},return_type:"any"},{enabled:!0,description:"Write a value to the active app's state. Keys are automatically scoped to the app.",name:"set_app_state",return_shape:"{ ok: true } on success. Throws if no app is currently active.",parameters:{key:"string",value:"any"},fn:async e=>{let{key:t,value:n}=e.params;if(!M)throw Error("No app is currently active");return e.util.update_workspace({["".concat(M.manifest.id,".").concat(t)]:n}),z&&z.syncWorkspace({[t]:n}),{ok:!0}},return_type:"object"}]}}},41020:function(e,t,n){n.d(t,{BQ:function(){return o},Ln:function(){return l}});var a=n(27573),r=n(7653),i=n(66276);function s(e){let t=e.space.unit;return{"--sc-background":e.color.background,"--sc-surface":e.color.surface,"--sc-surface-alt":e.color.surfaceAlt,"--sc-text":e.color.text,"--sc-text-muted":e.color.textMuted,"--sc-primary":e.color.primary,"--sc-accent":e.color.accent,"--sc-border":e.color.border,"--sc-danger":e.color.danger,"--sc-success":e.color.success,"--sc-warning":e.color.warning,"--sc-surface-elevated":e.surface.elevated,"--sc-surface-inset-highlight":e.surface.insetHighlight,"--sc-shadow-sm":e.shadow.sm,"--sc-shadow-md":e.shadow.md,"--sc-shadow-lg":e.shadow.lg,"--sc-shadow-xl":e.shadow.xl,"--sc-opacity-hover":String(e.opacity.hover),"--sc-opacity-pressed":String(e.opacity.pressed),"--sc-opacity-disabled":String(e.opacity.disabled),"--sc-opacity-overlay":String(e.opacity.overlay),"--sc-font-sans":e.typography.fontSans,"--sc-font-mono":e.typography.fontMono,"--sc-text-xs":e.typography.sizes.xs,"--sc-text-sm":e.typography.sizes.sm,"--sc-text-base":e.typography.sizes.base,"--sc-text-lg":e.typography.sizes.lg,"--sc-text-xl":e.typography.sizes.xl,"--sc-text-2xl":e.typography.sizes["2xl"],"--sc-font-weight-normal":String(e.typography.weights.normal),"--sc-font-weight-medium":String(e.typography.weights.medium),"--sc-font-weight-semibold":String(e.typography.weights.semibold),"--sc-font-weight-bold":String(e.typography.weights.bold),"--sc-leading-tight":String(e.typography.lineHeights.tight),"--sc-leading-normal":String(e.typography.lineHeights.normal),"--sc-leading-relaxed":String(e.typography.lineHeights.relaxed),"--sc-radius-sm":"".concat(e.radiu
1s.sm,"px"),"--sc-radius-md":"".concat(e.radius.md,"px"),"--sc-radius-lg":"".concat(e.radius.lg,"px"),"--sc-space-unit":"".concat(t,"px"),"--sc-space-0\\.5":"".concat(.5*t,"px"),"--sc-space-1":"".concat(t,"px"),"--sc-space-1\\.5":"".concat(1.5*t,"px"),"--sc-space-2":"".concat(2*t,"px"),"--sc-space-3":"".concat(3*t,"px"),"--sc-space-4":"".concat(4*t,"px"),"--sc-space-6":"".concat(6*t,"px"),"--sc-space-8":"".concat(8*t,"px"),"--sc-motion-fast":e.motion.durationFast,"--sc-motion-base":e.motion.durationBase,"--sc-motion-easing":e.motion.easing,"--sc-panel-style":e.componentRules.panelStyle,"--sc-button-style":e.componentRules.buttonStyle,"--sc-message-style":e.componentRules.messageStyle,"--sc-input-style":e.componentRules.inputStyle,"--sc-tooltip-style":e.componentRules.tooltipStyle,"--sc-divider-style":e.componentRules.dividerStyle,"--sc-badge-style":e.componentRules.badgeStyle,"--sc-focus-ring-style":e.componentRules.focusRingStyle,...function(e){let t="light"===e.mode,n=t?84:74,a=t?68:58,r=t?14:20,i=t?88:82,s=t?92:78,o=t?84:70,l=t?97:86;return{"--sc-surface-secondary":"color-mix(in srgb, ".concat(e.color.surface," ").concat(n,"%, ").concat(e.color.background," ").concat(100-n,"%)"),"--sc-surface-tertiary":"color-mix(in srgb, ".concat(e.color.surfaceAlt," ").concat(a,"%, ").concat(e.color.background," ").concat(100-a,"%)"),"--sc-surface-secondary-foreground":e.color.text,"--sc-surface-tertiary-foreground":e.color.textMuted,"--sc-separator":"color-mix(in srgb, ".concat(e.color.border," 88%, transparent)"),"--sc-overlay":"color-mix(in srgb, ".concat(e.color.background," ").concat(t?56:72,"%, transparent)"),"--sc-default":"color-mix(in srgb, ".concat(e.color.surfaceAlt," ").concat(t?78:66,"%, ").concat(e.color.background," ").concat(t?22:34,"%)"),"--sc-default-foreground":e.color.text,"--sc-default-hover":"color-mix(in srgb, ".concat(e.color.surfaceAlt," ").concat(i,"%, ").concat(e.color.text," ").concat(100-i,"%)"),"--sc-accent-soft":"color-mix(in srgb, ".concat(e.color.primary," ").concat(r,"%, ").concat(e.color.surface," ").concat(100-r,"%)"),"--sc-accent-soft-foreground":t?e.color.primary:e.color.text,"--sc-field-background":"color-mix(in srgb, ".concat(e.color.surface," ").concat(s,"%, ").concat(e.color.background," ").concat(100-s,"%)"),"--sc-field-foreground":e.color.text,"--sc-field-placeholder":"color-mix(in srgb, ".concat(e.color.textMuted," 76%, transparent)"),"--sc-field-hover":"color-mix(in srgb, ".concat(e.color.surfaceAlt," ").concat(o,"%, ").concat(e.color.surface," ").concat(100-o,"%)"),"--sc-field-focus":"color-mix(in srgb, ".concat(e.color.surface," ").concat(l,"%, ").concat(t?"#ffffff"
1:e.color.primary," ").concat(100-l,"%)"),"--sc-field-border":"color-mix(in srgb, ".concat(e.color.border," ").concat(t?100:92,"%, transparent)"),"--sc-field-border-hover":"color-mix(in srgb, ".concat(e.color.border," ").concat(t?78:72,"%, ").concat(e.color.text," ").concat(t?22:28,"%)"),"--sc-field-border-focus":"color-mix(in srgb, ".concat(e.color.primary," ").concat(t?46:58,"%, transparent)"),"--sc-status-focus-ring":"0 0 0 1px color-mix(in srgb, ".concat(e.color.primary," ").concat(t?40:55,"%, transparent), 0 0 0 4px color-mix(in srgb, ").concat(e.color.primary," ").concat(t?14:20,"%, transparent)"),"--sc-status-focus-field":"0 0 0 1px color-mix(in srgb, ".concat(e.color.primary," ").concat(t?45:58,"%, transparent), 0 0 0 4px color-mix(in srgb, ").concat(e.color.primary," ").concat(t?12:18,"%, transparent)"),"--sc-status-invalid-ring":"0 0 0 1px color-mix(in srgb, ".concat(e.color.danger," ").concat(t?42:55,"%, transparent), 0 0 0 4px color-mix(in srgb, ").concat(e.color.danger," ").concat(t?12:18,"%, transparent)"),"--sc-status-invalid-border":"color-mix(in srgb, ".concat(e.color.danger," ").concat(t?52:64,"%, transparent)"),"--sc-status-pending-opacity":"0.72"}}(e)}}function o(e){let t=Object.entries(e).map(e=>{let[t,n]=e;return" ".concat(t,": ").concat(n,";")});return":root {\n".concat(t.join("\n"),"\n}")}function l(e){let{children:t}=e,{pack:n}=(0,i.Q)();return(0,r.useLayoutEffect)(()=>{(function(e){let t=document.documentElement.style;for(let[n,a]of Object.entries(s(e)))t.setProperty(n,a);t.colorScheme=e.mode,document.documentElement.dataset.scMode=e.mode;let n=document.querySelector('meta[name="theme-color"]');n&&n.setAttribute("content",e.color.background)})(n),window.dispatchEvent(new CustomEvent("smartchats:theme_change",{detail:{tokens:s(n)}}))},[n]),(0,a.jsx)(a.Fragment,{children:t})}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.