@agentproto/apps 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +77 -4
- package/dist/index.mjs.map +1 -1
- package/dist/session-chat/index.d.ts +7 -0
- package/dist/session-chat/index.d.ts.map +1 -1
- package/dist/session-chat/panel.d.ts +1 -1
- package/dist/session-chat/panel.d.ts.map +1 -1
- package/dist/session-chat/panel.mjs +5 -0
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +10 -2
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.d.ts.map +1 -1
- package/dist/session-story/panel.mjs +8 -2
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +8 -2
- package/dist/session-story.mjs.map +1 -1
- package/dist/store/index.d.ts +61 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/panel.d.ts +53 -0
- package/dist/store/panel.d.ts.map +1 -0
- package/dist/store/panel.generated.d.ts +13 -0
- package/dist/store/panel.generated.d.ts.map +1 -0
- package/dist/store/panel.mjs +25 -0
- package/dist/store/panel.mjs.map +1 -0
- package/dist/store.mjs +71 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +13 -1
- package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +22 -2
- package/session-steward/.agentproto/workflows/session-steward/cron-rules.mjs +22 -4
- package/session-steward/.agentproto/workflows/session-steward/entry.mjs +64 -13
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/panel.generated.ts","../src/store/panel.ts","../src/store/index.ts"],"names":[],"mappings":";;;;;;;;;;AAYO,IAAM,oBAAA,GAAuB,mltCAAA;;;AC+B7B,IAAM,YAAA,GAAe,mBAAA;AAMrB,IAAM,aAAA,GAAgB,kBAAA;AAMtB,IAAM,cAAA,GAAiB;AAAA,EAC5B,aAAA;AAAA,EACA,UAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,eAAA;AAAA,EACA;AACF,CAAA;AAEO,IAAM,UAAA,GAAa,oBAAA;;;AC7BnB,IAAM,gBAAA,GAAmB,EAAE,MAAA,CAAO;AAAA,EACvC,UAAA,EAAY,CAAA,CACT,MAAA,EAAO,CACP,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAGN,CAAC;AAiCM,SAAS,aAId,GAAA,EAAoI;AACpI,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,aAAA;AAAA,IACJ,KAAA,EAAO,WAAA;AAAA,IACP,WAAA,EACE,qVAAA;AAAA,IAKF,WAAA,EAAa,gBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,KAAS;AACtB,MAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,WAAA,CAAY,MAAM,UAAU,CAAA;AACnD,MAAA,MAAM,WAAqB,EAAC;AAC5B,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAA,GAAA,KAAO;AACjC,QAAA,MAAM,IAAK,GAAA,CAAyC,QAAA;AACpD,QAAA,IAAI,CAAA,EAAG,QAAA,CAAS,IAAA,CAAK,GAAI,CAAuB,CAAA;AAChD,QAAA,OAAO,IAAA;AAAA,MACT,CAAC,CAAA;AACD,MAAA,OAAO;AAAA,QACL,OAAA;AAAA,QACA,SAAA,EAAW,IAAI,aAAA,EAAc;AAAA,QAC7B,OAAA,EAAS,IAAI,WAAA,EAAY;AAAA,QACzB,GAAI,QAAA,CAAS,MAAA,GAAS,IAAI,EAAE,QAAA,KAAa;AAAC,OAC5C;AAAA,IACF,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,WAAsB,SAAA,CAAU;AAAA,EAC3C,EAAA,EAAI,YAAA;AAAA,EACJ,IAAA,EAAM,WAAA;AAAA,EACN,WAAA,EACE,wKAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,WAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,cAAc;AAAA;AAE7B,CAAC","file":"store.mjs","sourcesContent":["/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/store/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-store-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:store\n *\n * `pnpm --filter @agentproto/apps run build:ui:store:check` (wired into CI)\n * fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const STORE_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto app store</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-store-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\").replace(/'/g, \\\"'\\\");\\n }\\n function text(value, fallback) {\\n return value && value.trim() !== \\\"\\\" ? esc(value) : esc(fallback);\\n }\\n function chips(row) {\\n const parts = [];\\n if (row.tier) parts.push(`<span class=\\\"chip\\\">${esc(row.tier)}</span>`);\\n if (row.placement) parts.push(`<span class=\\\"chip\\\">placement: ${esc(row.placement)}</span>`);\\n if (row.requires?.agentprotoVersion) {\\n parts.push(`<span class=\\\"chip\\\">requires ${esc(row.requires.agentprotoVersion)}</span>`);\\n }\\n if (row.requires?.apps && row.requires.apps.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires apps: ${esc(row.requires.apps.join(\\\", \\\"))}</span>`);\\n }\\n if (row.requires?.agents && row.requires.agents.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires agents: ${esc(row.requires.agents.join(\\\", \\\"))}</span>`);\\n }\\n return parts.join(\\\"\\\");\\n }\\n function installButton(catalogRow) {\\n return `<button class=\\\"sbtn store-install-btn\\\" data-appid=\\\"${esc(catalogRow.appId)}\\\" data-decision=\\\"install\\\">Install</button>`;\\n }\\n function renderCatalogFeatured(rows) {\\n const featured = rows.filter((r) => r.featured === true);\\n if (featured.length === 0) return \\\"\\\";\\n return featured.map(\\n (r) => `<div class=\\\"card store-card-featured\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`\\n ).join(\\\"\\\");\\n }\\n function renderCatalogAvailable(rows) {\\n const byCategory = /* @__PURE__ */ new Map();\\n for (const r of rows) {\\n if (r.installed) continue;\\n if (r.featured === true) continue;\\n const key = r.category && r.category !== \\\"\\\" ? r.category : \\\"Other\\\";\\n const list = byCategory.get(key);\\n if (list) list.push(r);\\n else byCategory.set(key, [r]);\\n }\\n let out = \\\"\\\";\\n for (const [category, list] of byCategory) {\\n out = out + `<h3 class=\\\"store-category-title\\\">${esc(category)}</h3>`;\\n for (const r of list) {\\n out = out + `<div class=\\\"card store-card-available\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`;\\n }\\n }\\n return out;\\n }\\n function renderInstalled(installed, catalogRows, updates) {\\n const updateIds = new Set(updates.map((u) => u.appId));\\n const byId = new Map(catalogRows.map((c) => [c.appId, c]));\\n if (installed.length === 0) return \\\"\\\";\\n return installed.map((app) => {\\n const version = app.version ?? byId.get(app.appId)?.version ?? \\\"\\\";\\n const source = app.source?.kind ?? \\\"local\\\";\\n const hasUi = byId.get(app.appId)?.hasUi === true || app.source?.kind === \\\"local\\\" && app.dir !== void 0;\\n const actions = [\\n `<button class=\\\"sbtn store-resync-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"resync\\\">Update</button>`,\\n `<button class=\\\"sbtn store-uninstall-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"uninstall\\\">Uninstall</button>`,\\n ...hasUi ? [\\n `<button class=\\\"sbtn store-open-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"open\\\">Open</button>`\\n ] : []\\n ];\\n return `<div class=\\\"card store-card-installed\\\" id=\\\"store-entry-${esc(app.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(app.name, app.appId)}</div>\\n <div class=\\\"card-actions-meta\\\"><span class=\\\"card-version\\\">${text(version, \\\"\\\")}</span><span class=\\\"card-source\\\">${esc(source)}</span>${updateIds.has(app.appId) ? `<span class=\\\"badge store-update-badge\\\">Update available</span>` : \\\"\\\"}</div>\\n <div class=\\\"card-actions\\\">${actions.join(\\\"\\\")}</div>\\n </div>`;\\n }).join(\\\"\\\");\\n }\\n function renderSourcesHeader(snapshot2) {\\n if (!snapshot2.warnings || snapshot2.warnings.length === 0) return \\\"\\\";\\n return `<div class=\\\"store-warnings\\\">${snapshot2.warnings.map((w) => `<p class=\\\"store-warning\\\">${esc(w)}</p>`).join(\\\"\\\")}</div>`;\\n }\\n function renderBuiltins(builtinNames) {\\n if (builtinNames.length === 0) return \\\"\\\";\\n const details = `<details class=\\\"store-builtins\\\"><summary>Builtin panels</summary><ul class=\\\"store-builtin-list\\\">${builtinNames.map((n) => `<li class=\\\"store-builtin-item\\\">${esc(n)}</li>`).join(\\\"\\\")}</ul></details>`;\\n return details;\\n }\\n function isAgentappUrl(url) {\\n return url.replace(/\\\\?.*$/, \\\"\\\").replace(/#.*$/, \\\"\\\").toLowerCase().endsWith(\\\".agentapp\\\");\\n }\\n function storeEmpty(catalogRows, installed) {\\n if (catalogRows.length > 0 || installed.length > 0) return \\\"\\\";\\n return `<div class=\\\"store-empty\\\">\\n <p class=\\\"store-empty-title\\\">No apps installed, and the app catalog is empty.</p>\\n <p class=\\\"store-empty-hint\\\">Add a <code>catalog.sources</code> entry to your daemon config, drop an <code>app-catalog.json</code> next to it, or install an app directly from a URL in the form below.</p>\\n </div>`;\\n }\\n function render(snapshot2) {\\n const installed = renderInstalled(snapshot2.installed, snapshot2.catalog, snapshot2.updates);\\n const featured = renderCatalogFeatured(snapshot2.catalog);\\n const available = renderCatalogAvailable(snapshot2.catalog);\\n const builtins = renderBuiltins([\\\"sessions-panel\\\", \\\"agents-overview\\\", \\\"bureau-sessions\\\", \\\"session-story\\\", \\\"live-session\\\", \\\"work-board\\\"]);\\n const sections = [];\\n sections.push(renderSourcesHeader(snapshot2));\\n if (installed !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Installed</h2>`, `<div class=\\\"grill\\\">${installed}</div>`);\\n }\\n if (featured !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Featured</h2>`, featured);\\n }\\n if (available !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Available</h2>`, `<div class=\\\"grill\\\">${available}</div>`);\\n }\\n if (installed === \\\"\\\" && featured === \\\"\\\" && available === \\\"\\\") {\\n sections.push(storeEmpty(snapshot2.catalog, snapshot2.installed));\\n }\\n sections.push(builtins);\\n return sections.join(\\\"\\\\n\\\");\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`app store: missing #${id}`);\\n return el;\\n }\\n let snapshot = { catalog: [], installed: [], updates: [] };\\n function setStatus(msg) {\\n getEl(\\\"statusbar\\\").textContent = msg;\\n }\\n function sendInstall(payload) {\\n return callTool(\\n \\\"app_install\\\",\\n payload\\n );\\n }\\n function runInstallConfirm(payload, ask) {\\n sendInstall(payload).then((result) => {\\n if (\\\"needsConfirmation\\\" in result && result.needsConfirmation === true) {\\n if (!ask(result)) return;\\n callTool(\\\"app_install\\\", {\\n ...payload,\\n confirm: result.confirm\\n }).then(() => {\\n setStatus(`Installed ${String(payload.url)}`);\\n return refresh();\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n } else {\\n setStatus(`Installed ${result.appId}`);\\n return refresh();\\n }\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n }\\n function buildInstallPayload(entry) {\\n const source = entry.source;\\n if (!source || typeof source !== \\\"object\\\") return void 0;\\n const s = source;\\n const payload = { url: s.url };\\n if (s.kind === \\\"agentapp\\\") {\\n payload.sha256 = s.sha256;\\n } else {\\n if (s.ref) payload.ref = s.ref;\\n if (s.subdir) payload.subdir = s.subdir;\\n if (s.sha) payload.sha = s.sha;\\n payload.allowBuild = false;\\n }\\n if (entry.catalogUrl) payload.catalogUrl = entry.catalogUrl;\\n return payload;\\n }\\n function describePreview(preview) {\\n const sha = preview.kind === \\\"agentapp\\\" ? preview.sha256 ? `sha256 ${preview.sha256}` : \\\"no pinned digest\\\" : preview.sha ? `commit ${preview.sha}` : \\\"no pinned commit\\\";\\n const buildLine = preview.runsBuildCommand ? \\\"It WILL run the app's ui.build command.\\\" : \\\"It will not run any build command.\\\";\\n return `Install ${preview.url}? (${preview.kind}; ${sha})\\n\\n${buildLine}\\n\\nConfirm to proceed.`;\\n }\\n function armInstall(entry) {\\n const payload = buildInstallPayload(entry);\\n if (!payload) {\\n setStatus(\\\"Catalog entry has no remote source — cannot install.\\\");\\n return;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function installFromUrl(url, sha256, ref, subdir) {\\n const payload = { url };\\n if (isAgentappUrl(url)) {\\n if (sha256) payload.sha256 = sha256;\\n } else {\\n if (ref) payload.ref = ref;\\n if (subdir) payload.subdir = subdir;\\n payload.allowBuild = false;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function resync(appId) {\\n callTool(\\\"app_resync\\\", { appId }).then((result) => {\\n setStatus(result.changed ? `${appId} updated` : `${appId} already up to date`);\\n return refresh();\\n }).catch((e) => setStatus(`Update failed: ${e.message}`));\\n }\\n function uninstall(appId) {\\n if (!window.confirm(`Uninstall ${appId}? Its data directory is kept.`)) return;\\n callTool(\\\"app_uninstall\\\", { appId }).then(() => {\\n setStatus(`Uninstalled ${appId}`);\\n return refresh();\\n }).catch((e) => setStatus(`Uninstall failed: ${e.message}`));\\n }\\n function openUi(appId) {\\n const encoded = appId.split(\\\"/\\\").map(encodeURIComponent).join(\\\"/\\\").replace(/%40/g, \\\"@\\\");\\n window.open(`/apps/${encoded}/ui`, \\\"_blank\\\", \\\"noopener\\\");\\n }\\n let deepLinked = false;\\n function deepLinkInstall() {\\n if (deepLinked) return;\\n const target = new URLSearchParams(window.location.search).get(\\\"install\\\");\\n if (!target) return;\\n deepLinked = true;\\n const entry = snapshot.catalog.find((c) => c.appId === target && !c.installed);\\n if (!entry) return;\\n const el = document.getElementById(`store-entry-${CSS.escape(target)}`);\\n if (el && typeof el.scrollIntoView === \\\"function\\\") el.scrollIntoView({ block: \\\"center\\\" });\\n armInstall(entry);\\n }\\n function refresh() {\\n return callTool(\\\"app_catalog\\\", {}).then((catalogRaw) => {\\n const snapshotRaw = catalogRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.catalog) ? snapshotRaw.catalog : Array.isArray(catalogRaw) ? catalogRaw : [];\\n snapshot.catalog = rows.filter((r) => r && r.appId);\\n return callTool(\\\"app_list\\\", {});\\n }).then((appsRaw) => {\\n const snapshotRaw = appsRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.installed) ? snapshotRaw.installed : Array.isArray(appsRaw) ? appsRaw : [];\\n snapshot.installed = rows.filter((a) => a && a.appId);\\n return callTool(\\\"app_updates\\\", {});\\n }).then((updatesRaw) => {\\n const sr = updatesRaw ?? {};\\n const rows = Array.isArray(sr.updates) ? sr.updates : Array.isArray(updatesRaw) ? updatesRaw : [];\\n snapshot.updates = rows.filter((u) => u && u.appId);\\n renderBody();\\n deepLinkInstall();\\n }).catch((e) => setStatus(`Error: ${e.message}`));\\n }\\n function renderBody() {\\n const content = getEl(\\\"content\\\");\\n content.innerHTML = render(snapshot);\\n }\\n function boot() {\\n const content = getEl(\\\"content\\\");\\n const urlBtn = document.getElementById(\\\"store-install-from-url-btn\\\");\\n const urlInput = document.getElementById(\\\"store-url\\\");\\n const shaInput = document.getElementById(\\\"store-sha256\\\");\\n const refInput = document.getElementById(\\\"store-ref\\\");\\n const subdirInput = document.getElementById(\\\"store-subdir\\\");\\n if (urlBtn && urlInput) {\\n urlBtn.addEventListener(\\\"click\\\", () => {\\n const url = urlInput.value.trim();\\n if (url === \\\"\\\") return;\\n installFromUrl(\\n url,\\n shaInput && shaInput.value.trim() !== \\\"\\\" ? shaInput.value.trim() : void 0,\\n refInput && refInput.value.trim() !== \\\"\\\" ? refInput.value.trim() : void 0,\\n subdirInput && subdirInput.value.trim() !== \\\"\\\" ? subdirInput.value.trim() : void 0\\n );\\n });\\n }\\n content.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-decision]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const appId = btn.getAttribute(\\\"data-appid\\\");\\n if (!appId) return;\\n const decision = btn.getAttribute(\\\"data-decision\\\");\\n if (decision === \\\"install\\\") {\\n const entry = snapshot.catalog.find((c) => c.appId === appId);\\n if (entry) armInstall(entry);\\n } else if (decision === \\\"resync\\\") {\\n resync(appId);\\n } else if (decision === \\\"uninstall\\\") {\\n uninstall(appId);\\n } else if (decision === \\\"open\\\") {\\n openUi(appId);\\n }\\n });\\n void initBridge().then(() => refresh()).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n The store panel's stylesheet — same two-sheet token mapping as\\n work-board's (--vscode-* wins when a host injects them, the agentproto\\n brand fallbacks otherwise; light/dark split via prefers-color-scheme\\n for a standalone tab). See that file's header for the full rationale\\n — the --st-* names here are this panel's own namespace.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --st-bg: var(--vscode-editor-background, #f5f6f3);\\n --st-surface: var(--vscode-sideBar-background, #ffffff);\\n --st-ink: var(--vscode-editor-foreground, #171a1e);\\n --st-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --st-border: var(--vscode-panel-border, #dde0da);\\n --st-accent: var(--vscode-button-background, #0f62fe);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --st-bg: var(--vscode-editor-background, #101214);\\n --st-surface: var(--vscode-sideBar-background, #16191d);\\n --st-ink: var(--vscode-editor-foreground, #e8eaed);\\n --st-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --st-border: var(--vscode-panel-border, #272c33);\\n --st-accent: var(--vscode-button-background, #4589ff);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n }\\n}\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--st-bg);\\n color: var(--st-ink);\\n overflow: hidden;\\n}\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#store-head {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--st-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--st-surface);\\n flex-shrink: 0;\\n font-weight: 600;\\n}\\n\\n#content { flex: 1; overflow-y: auto; padding: 12px; }\\n\\n.store-section-title {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n margin: 14px 0 8px;\\n}\\n.store-section-title:first-child { margin-top: 0; }\\n\\n.store-category-title { font-size: 12px; font-weight: 600; margin: 12px 0 6px; }\\n\\n.card {\\n background: var(--st-surface);\\n border: 1px solid var(--st-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n margin-bottom: 8px;\\n}\\n.card-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.card-desc { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 6px; }\\n.card-version, .card-source {\\n font-size: 10px;\\n color: var(--st-ink-muted);\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n margin-right: 6px;\\n}\\n.card-chips { margin-bottom: 6px; }\\n.card-actions { display: flex; gap: 4px; flex-wrap: wrap; }\\n\\n.chip {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 600;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-ink-muted) 15%, transparent);\\n color: var(--st-ink-muted);\\n margin-right: 4px;\\n}\\n\\n.badge {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 700;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-warn) 18%, transparent);\\n color: var(--st-warn);\\n margin-right: 4px;\\n}\\n\\n.sbtn {\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.sbtn:hover { background: var(--st-border); }\\n.sbtn.primary { background: var(--st-accent); color: var(--st-ink-on-accent); border-color: var(--st-accent); }\\n.sbtn.danger { border-color: var(--st-warn); color: var(--st-warn); }\\n\\n.store-url-install { display: flex; gap: 8px; margin: 6px 0 12px; }\\n.store-url-install input {\\n flex: 1;\\n min-width: 120px;\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#confirm-panel {\\n border: 1px solid var(--st-accent);\\n border-radius: 6px;\\n padding: 10px;\\n margin-bottom: 12px;\\n background: var(--st-surface);\\n}\\n#confirm-panel h2 { font-size: 13px; margin-bottom: 8px; }\\n#confirm-meta { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 8px; }\\n#confirm-meta .mono, .mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.build-flag { color: var(--st-warn); font-weight: 600; }\\n\\n.store-warnings { margin-bottom: 10px; }\\n.store-warning {\\n font-size: 11px;\\n color: var(--st-warn);\\n padding: 4px 8px;\\n border: 1px solid color-mix(in srgb, var(--st-warn) 40%, transparent);\\n border-radius: 4px;\\n margin-bottom: 4px;\\n word-break: break-word;\\n}\\n\\n.store-empty {\\n border: 1px dashed var(--st-border);\\n border-radius: 6px;\\n padding: 16px;\\n margin-bottom: 12px;\\n}\\n.store-empty-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; }\\n.store-empty-hint { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 10px; line-height: 1.5; }\\n.store-empty code {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n background: color-mix(in srgb, var(--st-ink-muted) 12%, transparent);\\n padding: 0 3px;\\n border-radius: 3px;\\n}\\n\\n.store-builtins { border-top: 1px solid var(--st-border); padding-top: 8px; margin-top: 12px; }\\n.store-builtins summary {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n cursor: pointer;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n}\\n.store-builtin-list { margin: 8px 0 0 16px; font-size: 11px; color: var(--st-ink-muted); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--st-ink-muted);\\n border-top: 1px solid var(--st-border);\\n background: var(--st-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"store-head\\\"><span>App Store</span></div>\\n <div id=\\\"confirm-panel\\\" hidden></div>\\n <div id=\\\"content\\\"></div>\\n <div id=\\\"url-install-static\\\">\\n <div class=\\\"store-url-install\\\">\\n <input id=\\\"store-url\\\" type=\\\"text\\\" placeholder=\\\"https://example.com/my-app.agentapp or https://github.com/user/repo\\\" />\\n <input id=\\\"store-sha256\\\" type=\\\"text\\\" placeholder=\\\"sha256 (.agentapp, optional)\\\" />\\n <input id=\\\"store-ref\\\" type=\\\"text\\\" placeholder=\\\"git ref (optional)\\\" />\\n <input id=\\\"store-subdir\\\" type=\\\"text\\\" placeholder=\\\"git subdir (optional)\\\" />\\n <button class=\\\"sbtn\\\" id=\\\"store-install-from-url-btn\\\">Install from URL</button>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto App Store panel — the `STORE_HTML`\n * served as the `ui://agentproto_store/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_store` tool defined in ./index.ts.\n *\n * The store over the daemon's app-catalog/install plane (packages/\n * runtime/src/app-tools.ts): Installed (version/source/Update-available\n * badge, resync + uninstall actions), Featured + Available (grouped by\n * category, tier/placement/requires chips), Install-from-URL (with the\n * `.agentapp`-vs-git detection), Sources (warnings + stale flags),\n * Builtin panels (collapsed) — and a never-blank empty state pointing at\n * how to add a catalog source.\n *\n * `STORE_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run\n * build:ui:store` (scripts/build-store-ui.mjs) — same committed-generated\n * contract as work-board (panel.generated.ts lives in git,\n * `build:ui:store:check` fails the CI drift gate). See ./ui/vite.config.ts\n * for how the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined\n * as a classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface).\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Read: tools/call → app_catalog / app_list / app_updates\n * – Apply (always through the SAME verbs every other caller uses — no\n * second install path): app_install (CONFIRMED, two-call handshake),\n * app_resync, app_uninstall\n */\n\nimport { STORE_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const STORE_APP_ID = \"@agentproto/store\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeStoreApp`),\n * hence its resource uri `ui://agentproto_store/view`. Follows the\n * convention of every other builtin panel (`agentproto_work_board`,\n * `agentproto_sessions`, …): `agentproto_<slug>`. */\nexport const STORE_TOOL_ID = \"agentproto_store\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const STORE_UI_TOOLS = [\n \"app_catalog\",\n \"app_list\",\n \"app_install\",\n \"app_resync\",\n \"app_updates\",\n \"app_uninstall\",\n \"app_status\",\n] as const\n\nexport const STORE_HTML = STORE_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto App Store panel — the\n * browse/install surface over `app_catalog`, `app_list`, `app_updates`\n * (+ apply verbs `app_install`/`app_resync`/`app_uninstall`), mounted by\n * the daemon in builtin-apps.ts (`makeBuiltinPanelApps`) exactly like\n * work-board.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). Host wiring: runtime's\n * builtin-apps.ts assembles the mounted list, with this factory closing\n * over the store's LIVE read paths (`listCatalog` is `dispatchTool\n * (\"app_catalog\")`-equivalent, `listInstalled` reads the live\n * AppRegistry) — so the store's execute() answer and what the same verbs\n * return can't drift.\n *\n * Install CONFIRMATION: the panel NEVER calls `app_install` silently.\n * Through the ui.tools bridge the FIRST call (no `confirm`) comes back a\n * preview (`{needsConfirmation: true, url, sha256|sha, runsBuildCommand}`);\n * the SECOND call repeats the same payload PLUS the returned `confirm`\n * token. Contained in the panel's dialog (ui/main.ts). Direct MCP/CLI\n * callers skip this entirely: the guard's preview costs them an extra\n * call only when they pass it, never a behavioural change (the guard\n * triggers on the panel's ui.tools tool-call path only — see\n * app-tools.ts's `appInstallConfirmation`).\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n STORE_APP_ID,\n STORE_HTML,\n STORE_TOOL_ID,\n STORE_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const storeInputSchema = z.object({\n catalogUrl: z\n .string()\n .optional()\n .describe(\n \"Restrict the snapshot to ONE catalog source (`catalog.sources[i].url`). \" +\n \"Omit for the merged listing app_catalog itself answers.\",\n ),\n})\n\nexport type StoreInput = z.infer<typeof storeInputSchema>\n\n/** The store's own read paths — mirrored off what the SAME MCP verbs\n * (`app_catalog`, `app_list`, `app_updates`) answer, so the panel's\n * snapshot can never disagree with the returned rows. Generic over the\n * row shape (this package doesn't own those record types — they live in\n * @agentproto/runtime's app-registry / app-catalog) so the host supplies\n * the concrete type argument when wiring, mirroring\n * `WorkBoardOps<TTask>`/`SessionsPanelOps<TSession>`. */\nexport interface StoreOps<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n listCatalog(catalogUrl?: string): Promise<ReadonlyArray<TCatalogRow>>\n listInstalled(): ReadonlyArray<TInstalledRow>\n listUpdates(): ReadonlyArray<TUpdateRow>\n}\n\nexport interface StoreOutput<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n /** `app_catalog`'s entries — featured/uninstalled ones grouped into the\n * Available shelf by `category`; `updateAvailable` rows read the\n * Update-available badge. */\n catalog: ReadonlyArray<TCatalogRow>\n /** `app_list`'s installed records — the Installed shelf. */\n installed: ReadonlyArray<TInstalledRow>\n /** `app_updates`' update entries — empty when nothing tracked is stale. */\n updates: ReadonlyArray<TUpdateRow>\n /** Warnings from the catalog sources (a failing source becomes a\n * warning, never an error — app-tools.ts's app_catalog contract). */\n warnings?: readonly string[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeStoreApp<\n TCatalogRow = unknown,\n TInstalledRow = unknown,\n TUpdateRow = unknown,\n>(ops: StoreOps<TCatalogRow, TInstalledRow, TUpdateRow>): AgnoMcpApp<StoreInput, StoreOutput<TCatalogRow, TInstalledRow, TUpdateRow>> {\n return {\n id: STORE_TOOL_ID,\n title: \"App Store\",\n description:\n \"Open the agentproto App Store — browse/install apps from the daemon's \" +\n \"app_catalog, resync and uninstall installed ones. Sections: Installed \" +\n \"(with an Update-available badge) / Featured+Available from the catalog / \" +\n \"Install-from-URL / Sources / Builtin panels (the `?install=<appId>` \" +\n \"deep link pre-opens an entry's install confirmation).\",\n inputSchema: storeInputSchema,\n execute: async input => {\n const rows = await ops.listCatalog(input.catalogUrl)\n const warnings: string[] = []\n const catalog = rows.filter(row => {\n const w = (row as { warnings?: readonly string[] }).warnings\n if (w) warnings.push(...(w as readonly string[]))\n return true\n })\n return {\n catalog,\n installed: ops.listInstalled(),\n updates: ops.listUpdates(),\n ...(warnings.length > 0 ? { warnings } : {}),\n }\n },\n html: STORE_HTML,\n }\n}\n\nexport const storeApp: AppHandle = defineApp({\n id: STORE_APP_ID,\n name: \"App Store\",\n description:\n \"Open the agentproto App Store — installed apps, catalog shelves (featured/available), \" +\n \"install-from-URL with an explicit confirmation, sources and builtin panels.\",\n agents: [],\n ui: {\n html: STORE_HTML,\n title: \"App Store\",\n tools: [...STORE_UI_TOOLS],\n },\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentproto/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "@agentproto/apps — a home for ready-made agentproto apps (teams of agents + their workflows), declared with @agentproto/app-kit. Import a team (code-team, content-team, …) and use any of its agents/workflows from your own host: in-process via toMastraAgents(), or emit its manifests to disk.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentproto",
|
|
@@ -139,6 +139,16 @@
|
|
|
139
139
|
"import": "./dist/work-board/panel.mjs",
|
|
140
140
|
"default": "./dist/work-board/panel.mjs"
|
|
141
141
|
},
|
|
142
|
+
"./store": {
|
|
143
|
+
"types": "./dist/store/index.d.ts",
|
|
144
|
+
"import": "./dist/store.mjs",
|
|
145
|
+
"default": "./dist/store.mjs"
|
|
146
|
+
},
|
|
147
|
+
"./store/panel": {
|
|
148
|
+
"types": "./dist/store/panel.d.ts",
|
|
149
|
+
"import": "./dist/store/panel.mjs",
|
|
150
|
+
"default": "./dist/store/panel.mjs"
|
|
151
|
+
},
|
|
142
152
|
"./review-panel": {
|
|
143
153
|
"types": "./dist/review-panel/index.d.ts",
|
|
144
154
|
"import": "./dist/review-panel.mjs",
|
|
@@ -192,6 +202,8 @@
|
|
|
192
202
|
"test:watch": "vitest",
|
|
193
203
|
"build:ui": "tsc --noEmit -p src/work-board/ui/tsconfig.json && node scripts/build-work-board-ui.mjs",
|
|
194
204
|
"build:ui:check": "tsc --noEmit -p src/work-board/ui/tsconfig.json && node scripts/build-work-board-ui.mjs --check",
|
|
205
|
+
"build:ui:store": "tsc --noEmit -p src/store/ui/tsconfig.json && node scripts/build-store-ui.mjs",
|
|
206
|
+
"build:ui:store:check": "tsc --noEmit -p src/store/ui/tsconfig.json && node scripts/build-store-ui.mjs --check",
|
|
195
207
|
"build:ui:reviews": "tsc --noEmit -p src/review-panel/ui/tsconfig.json && node scripts/build-review-panel-ui.mjs",
|
|
196
208
|
"build:ui:reviews:check": "tsc --noEmit -p src/review-panel/ui/tsconfig.json && node scripts/build-review-panel-ui.mjs --check"
|
|
197
209
|
}
|
|
@@ -20,6 +20,12 @@ inputs:
|
|
|
20
20
|
type: boolean
|
|
21
21
|
description: Close/flag sessions. False = dry run (plan + verdicts, no mutation).
|
|
22
22
|
default: false
|
|
23
|
+
relabelWindowHours:
|
|
24
|
+
type: number
|
|
25
|
+
description: >-
|
|
26
|
+
Only terminal sessions that ended within this many hours are listed as
|
|
27
|
+
relabel candidates (at most 20, newest first).
|
|
28
|
+
default: 24
|
|
23
29
|
minConfidence:
|
|
24
30
|
type: number
|
|
25
31
|
description: Judge confidence needed to act on a verdict.
|
|
@@ -62,7 +68,9 @@ inputs:
|
|
|
62
68
|
type: string
|
|
63
69
|
description: >-
|
|
64
70
|
Installed app whose `app_state` ledger holds the verdict memory.
|
|
65
|
-
Default
|
|
71
|
+
Default `@agentproto/session-steward` (the id in APP.md). A bare name
|
|
72
|
+
also matches a scoped install; a missing app means no memory, noted in
|
|
73
|
+
the report.
|
|
66
74
|
stableVerdictPasses:
|
|
67
75
|
type: number
|
|
68
76
|
description: >-
|
|
@@ -110,6 +118,7 @@ steps:
|
|
|
110
118
|
tool: session_wrapup_plan
|
|
111
119
|
inputs:
|
|
112
120
|
idleMinutes: $steps.settings.idleMinutes
|
|
121
|
+
wait: true
|
|
113
122
|
|
|
114
123
|
- id: candidates
|
|
115
124
|
kind: transform
|
|
@@ -164,10 +173,21 @@ steps:
|
|
|
164
173
|
verdict: $item.verdict
|
|
165
174
|
note: $item.note
|
|
166
175
|
|
|
176
|
+
- id: installedApps
|
|
177
|
+
kind: tool
|
|
178
|
+
name: List installed apps (to resolve the memory app)
|
|
179
|
+
tool: app_list
|
|
180
|
+
inputs: {}
|
|
181
|
+
|
|
182
|
+
- id: memoryApp
|
|
183
|
+
kind: transform
|
|
184
|
+
name: Resolve the installed app holding the verdict memory
|
|
185
|
+
description: Entry-based — resolveMemoryApp. No such app ⇒ no memory + a report note.
|
|
186
|
+
|
|
167
187
|
- id: memoryQueue
|
|
168
188
|
kind: transform
|
|
169
189
|
name: Memory read queue
|
|
170
|
-
description: Entry-based. Empty when no
|
|
190
|
+
description: Entry-based. Empty when no installed memory app resolves.
|
|
171
191
|
|
|
172
192
|
- id: memoryRead
|
|
173
193
|
kind: map
|
|
@@ -186,13 +186,31 @@ export function detectStall(input = {}) {
|
|
|
186
186
|
|
|
187
187
|
// ── never-ran (mission item 3) ───────────────────────────────────────────
|
|
188
188
|
|
|
189
|
+
const minutesSince = (ts, nowMs) => {
|
|
190
|
+
const ms = ts ? Date.parse(ts) : Number.NaN
|
|
191
|
+
return Number.isFinite(ms) ? (nowMs - ms) / 60_000 : undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
189
194
|
/**
|
|
190
195
|
* A session that never actually ran: explicit 0 in AND 0 out. `undefined`
|
|
191
|
-
* tokens (not reported) is NOT "never ran" — only a hard 0/0 is.
|
|
192
|
-
*
|
|
196
|
+
* tokens (not reported) is NOT "never ran" — only a hard 0/0 is. A session
|
|
197
|
+
* that is busy, still starting/provisioning, or has a prompt queued for its
|
|
198
|
+
* first turn is merely young, not stuck. With `opts.nowMs`, the session must
|
|
199
|
+
* also be at least `opts.idleMinutes` old (`startedAt`) AND idle
|
|
200
|
+
* (`lastActivityAt`, else `startedAt`); an absent timestamp does not block.
|
|
193
201
|
*/
|
|
194
|
-
export function isNeverRan(session) {
|
|
195
|
-
|
|
202
|
+
export function isNeverRan(session, opts = {}) {
|
|
203
|
+
if (!(session?.tokensIn === 0 && session?.tokensOut === 0)) return false
|
|
204
|
+
if (session.busy === true || session.status === "starting" || session.provisioning) return false
|
|
205
|
+
if (Array.isArray(session.pendingPrompts) && session.pendingPrompts.length > 0) return false
|
|
206
|
+
if (typeof opts.nowMs === "number") {
|
|
207
|
+
const threshold = opts.idleMinutes ?? 0
|
|
208
|
+
const age = minutesSince(session.startedAt, opts.nowMs)
|
|
209
|
+
const idle = minutesSince(session.lastActivityAt ?? session.startedAt, opts.nowMs)
|
|
210
|
+
if (age !== undefined && age < threshold) return false
|
|
211
|
+
if (idle !== undefined && idle < threshold) return false
|
|
212
|
+
}
|
|
213
|
+
return true
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
// ── fast-path done (mission item 4) ──────────────────────────────────────
|
|
@@ -55,11 +55,17 @@ import {
|
|
|
55
55
|
|
|
56
56
|
const DEFAULT_IDLE_MINUTES = 30
|
|
57
57
|
const DEFAULT_MIN_CONFIDENCE = 0.8
|
|
58
|
+
/** Terminal sessions older than this (hours since they ended) are not listed
|
|
59
|
+
* as relabel candidates. */
|
|
60
|
+
const DEFAULT_RELABEL_WINDOW_HOURS = 24
|
|
61
|
+
/** Most relabel candidates printed in the report (newest first). */
|
|
62
|
+
export const RELABEL_MAX_LINES = 20
|
|
58
63
|
/** How many consecutive passes on an unchanged fingerprint before the judge
|
|
59
64
|
* cache stops re-judging a session. */
|
|
60
65
|
const DEFAULT_STABLE_VERDICT_PASSES = 2
|
|
61
|
-
/** The installed app whose `app_state` ledger holds the verdict memory
|
|
62
|
-
|
|
66
|
+
/** The installed app whose `app_state` ledger holds the verdict memory — the
|
|
67
|
+
* `id` in APP.md, which is what the daemon's app registry keys installs by. */
|
|
68
|
+
const DEFAULT_APP_ID = "@agentproto/session-steward"
|
|
63
69
|
// The agent judge's model is the `judge.session` model ROLE, resolved at run
|
|
64
70
|
// time by the `modelRoles` step (the daemon's `model_roles` tool): explicit
|
|
65
71
|
// `judgeModel` input > repo agentproto.json `models` > daemon config `models`
|
|
@@ -103,6 +109,7 @@ export function resolveSettings(input, modelRoles) {
|
|
|
103
109
|
return {
|
|
104
110
|
idleMinutes: Math.floor(num(i.idleMinutes, DEFAULT_IDLE_MINUTES, { min: 1 })),
|
|
105
111
|
apply: i.apply === true,
|
|
112
|
+
relabelWindowHours: num(i.relabelWindowHours, DEFAULT_RELABEL_WINDOW_HOURS, { min: 0 }),
|
|
106
113
|
minConfidence: num(i.minConfidence, DEFAULT_MIN_CONFIDENCE, { max: 1 }),
|
|
107
114
|
judgeModel: explicitOrRole(i.judgeModel, modelRoles, ROLE_JUDGE_SESSION),
|
|
108
115
|
judge: JUDGE_BACKENDS.includes(i.judge) ? i.judge : "auto",
|
|
@@ -111,7 +118,7 @@ export function resolveSettings(input, modelRoles) {
|
|
|
111
118
|
askSessions: i.askSessions === true,
|
|
112
119
|
callerSessionId: typeof i.callerSessionId === "string" && i.callerSessionId ? i.callerSessionId : null,
|
|
113
120
|
callerOrigin: typeof i.callerOrigin === "string" && i.callerOrigin ? i.callerOrigin : null,
|
|
114
|
-
appId: typeof i.appId === "string"
|
|
121
|
+
appId: typeof i.appId === "string" ? i.appId.trim() : DEFAULT_APP_ID,
|
|
115
122
|
stableVerdictPasses: Math.floor(num(i.stableVerdictPasses, DEFAULT_STABLE_VERDICT_PASSES, { min: 1 })),
|
|
116
123
|
userOrigins: originPolicy.userOrigins,
|
|
117
124
|
closableOrigins: originPolicy.closableOrigins,
|
|
@@ -636,14 +643,23 @@ export function buildReport(b) {
|
|
|
636
643
|
// Terminal sessions with no outcome (mission item 5).
|
|
637
644
|
const relabel = b.steps.relabelQueue ?? []
|
|
638
645
|
if (relabel.length > 0) {
|
|
646
|
+
const total = b.steps.scan?.counts?.relabelTotal ?? relabel.length
|
|
647
|
+
const byVerdict = new Map()
|
|
648
|
+
for (const r of relabel) byVerdict.set(r.proposedVerdict, (byVerdict.get(r.proposedVerdict) ?? 0) + 1)
|
|
649
|
+
const perLabel = [...byVerdict].map(([v, n]) => `${v}: ${n}`).join(", ")
|
|
639
650
|
lines.push("", "## Terminal sessions missing an outcome (relabel candidates)")
|
|
640
|
-
|
|
651
|
+
lines.push(`- ${relabel.length} ended in the last ${s.relabelWindowHours}h (${total} without an outcome in all) — ${perLabel}`)
|
|
652
|
+
for (const r of relabel.slice(0, RELABEL_MAX_LINES)) lines.push(`- ${r.sessionId} → ${r.proposedVerdict} — ${r.reason}`)
|
|
653
|
+
const hidden = total - Math.min(relabel.length, RELABEL_MAX_LINES)
|
|
654
|
+
if (hidden > 0) lines.push(`- … and ${hidden} more (older/omitted)`)
|
|
641
655
|
}
|
|
642
656
|
|
|
643
657
|
// Verdict memory / cache (mission item 10).
|
|
644
658
|
const memory = b.steps.memory
|
|
645
659
|
const cachedCount = verdicts.filter(r => r.source === "cache").length
|
|
646
|
-
if (
|
|
660
|
+
if (b.steps.memoryApp?.note) {
|
|
661
|
+
lines.push(`- verdict memory: ${b.steps.memoryApp.note}`)
|
|
662
|
+
} else if (memory instanceof Map && memory.size > 0) {
|
|
647
663
|
lines.push(`- verdict memory: ${memory.size} session(s) known` + (cachedCount > 0 ? `, ${cachedCount} served from cache` : ""))
|
|
648
664
|
}
|
|
649
665
|
|
|
@@ -660,7 +676,11 @@ function idleMinutesOf(row, nowMs) {
|
|
|
660
676
|
return Number.isFinite(ms) ? Math.max(0, (nowMs - ms) / 60_000) : 0
|
|
661
677
|
}
|
|
662
678
|
|
|
663
|
-
|
|
679
|
+
/** `session_list` rows under any of its shapes: the un-paged `{sessions}`
|
|
680
|
+
* wrapper (what a workflow tool step gets), the paged `{items}` envelope, or
|
|
681
|
+
* a bare array. */
|
|
682
|
+
export function liveRowsOf(liveSessions) {
|
|
683
|
+
if (Array.isArray(liveSessions?.sessions)) return liveSessions.sessions
|
|
664
684
|
if (Array.isArray(liveSessions?.items)) return liveSessions.items
|
|
665
685
|
if (Array.isArray(liveSessions)) return liveSessions
|
|
666
686
|
return []
|
|
@@ -678,6 +698,8 @@ export function scanLive(liveSessions, settings, nowMs) {
|
|
|
678
698
|
const policy = policyOf(settings)
|
|
679
699
|
const self = settings?.callerSessionId ?? null
|
|
680
700
|
const idleThreshold = settings?.idleMinutes ?? DEFAULT_IDLE_MINUTES
|
|
701
|
+
const relabelWindowMs = (settings?.relabelWindowHours ?? DEFAULT_RELABEL_WINDOW_HOURS) * 3_600_000
|
|
702
|
+
let relabelTotal = 0
|
|
681
703
|
const busy = []
|
|
682
704
|
const idle = []
|
|
683
705
|
const terminal = []
|
|
@@ -717,7 +739,12 @@ export function scanLive(liveSessions, settings, nowMs) {
|
|
|
717
739
|
terminal.push({ sessionId: id, origin: s.origin, originClass, label })
|
|
718
740
|
const cand = terminalRelabelCandidate(s)
|
|
719
741
|
if (cand.candidate) {
|
|
720
|
-
|
|
742
|
+
relabelTotal++
|
|
743
|
+
const endedAt = s.endedAt ?? s.lastActivityAt ?? s.startedAt
|
|
744
|
+
const endedMs = endedAt ? Date.parse(endedAt) : Number.NaN
|
|
745
|
+
if (Number.isFinite(endedMs) && nowMs - endedMs <= relabelWindowMs) {
|
|
746
|
+
terminalRelabel.push({ sessionId: id, origin: s.origin, originClass, label, proposedVerdict: cand.proposedVerdict, reason: cand.reason, endedAt, endedMs })
|
|
747
|
+
}
|
|
721
748
|
}
|
|
722
749
|
continue
|
|
723
750
|
}
|
|
@@ -728,7 +755,7 @@ export function scanLive(liveSessions, settings, nowMs) {
|
|
|
728
755
|
continue
|
|
729
756
|
}
|
|
730
757
|
const row = { sessionId: id, origin: s.origin, originClass, label, idleMinutes, lastTurnErroredAt: s.lastTurnErroredAt ?? null }
|
|
731
|
-
if (isNeverRan(s)) neverRan.push(row)
|
|
758
|
+
if (isNeverRan(s, { nowMs, idleMinutes: idleThreshold })) neverRan.push(row)
|
|
732
759
|
if (s.busy === true) {
|
|
733
760
|
busy.push(row)
|
|
734
761
|
loopQueue.push({ sessionId: id, originClass, label })
|
|
@@ -743,6 +770,7 @@ export function scanLive(liveSessions, settings, nowMs) {
|
|
|
743
770
|
idle: idle.length,
|
|
744
771
|
terminal: terminal.length,
|
|
745
772
|
terminalRelabel: terminalRelabel.length,
|
|
773
|
+
relabelTotal,
|
|
746
774
|
neverRan: neverRan.length,
|
|
747
775
|
excluded: excluded.length,
|
|
748
776
|
}
|
|
@@ -792,6 +820,25 @@ export function analyzeStalls(scan, nowMs) {
|
|
|
792
820
|
}))
|
|
793
821
|
}
|
|
794
822
|
|
|
823
|
+
/** Ids out of an `app_list` result (bare array, `{apps}` or `{items}`). */
|
|
824
|
+
function installedAppIdsOf(appList) {
|
|
825
|
+
const rows = Array.isArray(appList) ? appList : Array.isArray(appList?.apps) ? appList.apps : Array.isArray(appList?.items) ? appList.items : []
|
|
826
|
+
return rows.map(r => (typeof r === "string" ? r : r?.appId ?? r?.id)).filter(id => typeof id === "string" && id)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Which installed app holds the verdict memory. An exact id match wins; a
|
|
830
|
+
* bare name (`session-steward`) also matches a scoped install
|
|
831
|
+
* (`@agentproto/session-steward`). A missing app, or `appId: ""`, is "no
|
|
832
|
+
* memory" with a note for the report — never a failed step. */
|
|
833
|
+
export function resolveMemoryApp(settings, appList) {
|
|
834
|
+
const wanted = settings?.appId
|
|
835
|
+
if (!wanted) return { appId: null, note: "off (appId empty)" }
|
|
836
|
+
const installed = installedAppIdsOf(appList)
|
|
837
|
+
const hit = installed.find(id => id === wanted) ?? installed.find(id => id.endsWith(`/${wanted}`))
|
|
838
|
+
if (hit) return { appId: hit, note: null }
|
|
839
|
+
return { appId: null, note: `off — no installed app "${wanted}" (install it, or pass its installed id as appId); no verdict was read or recorded` }
|
|
840
|
+
}
|
|
841
|
+
|
|
795
842
|
/** Fold the `app_state` read into the per-session verdict memory map. */
|
|
796
843
|
export function foldMemory(b) {
|
|
797
844
|
const events = settled(b.steps.memoryRead).ok.flatMap(r => (Array.isArray(r.value?.events) ? r.value.events : []))
|
|
@@ -845,9 +892,10 @@ export function buildProposalsStep(scan, loopResults, settings, nowMs) {
|
|
|
845
892
|
return { proposals, observed, stalls }
|
|
846
893
|
}
|
|
847
894
|
|
|
848
|
-
/** Terminal sessions missing an outcome
|
|
895
|
+
/** Terminal sessions missing an outcome that ended within the relabel
|
|
896
|
+
* window, newest first (the report caps how many it prints). */
|
|
849
897
|
export function buildRelabelQueue(scan) {
|
|
850
|
-
return (scan?.terminalRelabel ?? []).map(t => ({ ...t }))
|
|
898
|
+
return (scan?.terminalRelabel ?? []).map(t => ({ ...t })).sort((a, b) => b.endedMs - a.endedMs)
|
|
851
899
|
}
|
|
852
900
|
|
|
853
901
|
/** The `app_state` events to append for this pass's verdicts. The memory is
|
|
@@ -891,6 +939,7 @@ export default {
|
|
|
891
939
|
inputs: {
|
|
892
940
|
idleMinutes: { type: "number", description: `Idle threshold in minutes. Default ${DEFAULT_IDLE_MINUTES}.`, default: DEFAULT_IDLE_MINUTES },
|
|
893
941
|
apply: { type: "boolean", description: "Close/flag sessions. Default false = dry run (plan + verdicts, no mutation).", default: false },
|
|
942
|
+
relabelWindowHours: { type: "number", description: `Only terminal sessions that ended within this many hours are listed as relabel candidates (at most ${RELABEL_MAX_LINES}, newest first). Default ${DEFAULT_RELABEL_WINDOW_HOURS}.`, default: DEFAULT_RELABEL_WINDOW_HOURS },
|
|
894
943
|
minConfidence: { type: "number", description: `Judge confidence needed to act. Default ${DEFAULT_MIN_CONFIDENCE}.`, default: DEFAULT_MIN_CONFIDENCE },
|
|
895
944
|
judge: { type: "string", description: "Judge backend: `auto` (Jev when JEV_API_KEY resolves, else the agent judge), `jev`, or `agent`. A Jev failure always falls back to the agent judge. Default auto.", default: "auto" },
|
|
896
945
|
jevModel: { type: "string", description: `Jev model. Default ${DEFAULT_JEV_MODEL}.`, default: DEFAULT_JEV_MODEL },
|
|
@@ -917,7 +966,7 @@ export default {
|
|
|
917
966
|
id: "plan",
|
|
918
967
|
kind: "tool",
|
|
919
968
|
tool: "session_wrapup_plan",
|
|
920
|
-
inputs: { idleMinutes: "$steps.settings.idleMinutes" },
|
|
969
|
+
inputs: { idleMinutes: "$steps.settings.idleMinutes", wait: true },
|
|
921
970
|
},
|
|
922
971
|
{ id: "candidates", kind: "transform", compute: b => splitCandidates(b.steps.plan, b.steps.settings) },
|
|
923
972
|
// Host saturation header (report only — mission item 9) and the live
|
|
@@ -946,7 +995,9 @@ export default {
|
|
|
946
995
|
},
|
|
947
996
|
// Verdict memory (item 10): read the app_state ledger best-effort. The
|
|
948
997
|
// map is empty when no app id is set, so a caller can turn memory off.
|
|
949
|
-
{ id: "
|
|
998
|
+
{ id: "installedApps", kind: "tool", tool: "app_list", inputs: {} },
|
|
999
|
+
{ id: "memoryApp", kind: "transform", compute: b => resolveMemoryApp(b.steps.settings, b.steps.installedApps) },
|
|
1000
|
+
{ id: "memoryQueue", kind: "transform", compute: b => (b.steps.memoryApp?.appId ? [{ appId: b.steps.memoryApp.appId }] : []) },
|
|
950
1001
|
{
|
|
951
1002
|
id: "memoryRead",
|
|
952
1003
|
kind: "map",
|
|
@@ -1122,7 +1173,7 @@ export default {
|
|
|
1122
1173
|
},
|
|
1123
1174
|
// Verdict memory write-back (item 10) — a ledger append, never a session
|
|
1124
1175
|
// action; best-effort (an uninstalled app just yields no memory).
|
|
1125
|
-
{ id: "memoryWriteQueue", kind: "transform", compute: b => buildMemoryWriteQueue(b.steps.finalVerdicts, b.steps.settings) },
|
|
1176
|
+
{ id: "memoryWriteQueue", kind: "transform", compute: b => buildMemoryWriteQueue(b.steps.finalVerdicts, { ...b.steps.settings, appId: b.steps.memoryApp?.appId ?? "" }) },
|
|
1126
1177
|
{
|
|
1127
1178
|
id: "memoryWrite",
|
|
1128
1179
|
kind: "map",
|