PageSourceSearch

https://www.smartchats.ai/_next/static/chunks/564.ab33386abb60856a.js

js smartchats.ai collected 2026-10-05 15:06:27 UTC 224,971 bytes, 1 lines download raw bytes

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;\">&#9654;</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, '&lt;') + '</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 ? '&#9654;' : '&#9660;';\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\">&#x21bb;</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 = '&#9998;';\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 = '&#128465;';\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 = '&#128465;';\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\')">&larr; 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\')">&larr; 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.