harness-wingman 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -6,4 +6,4 @@ Not your copilot. Your wingman.
6
6
  npx harness-wingman serve
7
7
  ```
8
8
 
9
- 面板:http://127.0.0.1:7333 · 只绑 127.0.0.1,数据不出本机,零遥测。
9
+ 面板:http://127.0.0.1:7333 · 只绑 127.0.0.1。匿名使用统计不含内容与路径(明细 getwingman.dev/data);内容样本仅在你主动分享时发送。
@@ -11767,7 +11767,7 @@ function createReceiver(options = {}) {
11767
11767
  }
11768
11768
  accept({ harness, received_at: (/* @__PURE__ */ new Date()).toISOString(), payload: parsed });
11769
11769
  sendText(res, 200, "ok");
11770
- }, () => sendText(res, 400, "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25"));
11770
+ }, (error) => sendText(res, 400, error instanceof Error ? error.message : "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25"));
11771
11771
  }
11772
11772
  function handleEvents(req, res) {
11773
11773
  if (req.method !== "GET") {
@@ -11824,10 +11824,20 @@ function isHookPayload(value) {
11824
11824
  const name = value.hook_event_name;
11825
11825
  return typeof name === "string" && name.length > 0;
11826
11826
  }
11827
+ var HOOK_BODY_LIMIT = 1024 * 1024;
11827
11828
  function readBody(req) {
11828
11829
  return new Promise((resolve7, reject) => {
11829
11830
  const chunks = [];
11830
- req.on("data", (chunk) => chunks.push(chunk));
11831
+ let size = 0;
11832
+ req.on("data", (chunk) => {
11833
+ size += chunk.length;
11834
+ if (size > HOOK_BODY_LIMIT) {
11835
+ reject(new Error("body \u8D85\u8FC7 1MB \u4E0A\u9650"));
11836
+ req.destroy();
11837
+ return;
11838
+ }
11839
+ chunks.push(chunk);
11840
+ });
11831
11841
  req.on("end", () => resolve7(Buffer.concat(chunks).toString("utf8")));
11832
11842
  req.on("error", reject);
11833
11843
  });
@@ -13515,6 +13525,13 @@ function listen(server, port) {
13515
13525
  }
13516
13526
  async function handle(req, res, options, warn, runtime) {
13517
13527
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
13528
+ if (req.method !== "GET") {
13529
+ const origin = req.headers.origin;
13530
+ if (typeof origin === "string" && origin !== "" && !/^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin)) {
13531
+ sendJson(res, 403, { error: `\u62D2\u7EDD\u5F02\u6E90\u5199\u8BF7\u6C42(Origin: ${origin})` });
13532
+ return;
13533
+ }
13534
+ }
13518
13535
  if (options.receiver && url.pathname === "/hook") {
13519
13536
  options.receiver.handleHook(req, res);
13520
13537
  return;
@@ -0,0 +1,202 @@
1
+ (function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))s(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const c of i.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&s(c)}).observe(document,{childList:!0,subtree:!0});function t(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(a){if(a.ep)return;a.ep=!0;const i=t(a);fetch(a.href,i)}})();const L=["base","rules","skills","tools","gates","history"],E={base:"基础指令",rules:"你的规则",skills:"技能包",tools:"工具定义",gates:"闸门 hooks",history:"对话历史"},M={base:"#3987e5",rules:"#d95926",skills:"#199e70",tools:"#c98500",gates:"#898781",history:"#d55181"},T={cc:"Claude Code",codex:"Codex",dsh:"dsh"},K={cc:{base:"内置 system prompt",rules:"CLAUDE.md · rules/*.md",skills:"Skills · SKILL.md",tools:"MCP servers",gates:"hooks · permissions",history:"projects/*.jsonl"},codex:{base:"内置 instructions",rules:"AGENTS.md · config.toml",skills:"plugins",tools:"mcp_servers",gates:"approval_policy · hooks",history:"history.jsonl"},dsh:{base:"core 插件(一切皆插件)",rules:"profile · patch",skills:"skill 插件",tools:"tool-* 插件",gates:"approval · permission 插件",history:"session log 插件"}};function y(e){return e<1e3?String(e):`${(Math.round(e/100)/10).toFixed(1)}k`}function k(e){if(!e)return"—";const n=y(e.value);return e.kind==="estimate"?`约 ${n}`:n}function Ee(e){return e.onTriggerTokens?`常驻 ${k(e.tokens)} · 触发后 ${k(e.onTriggerTokens)}`:k(e.tokens)}function Y(e){let n=0,t=!1,s="exact";for(const a of e)!a.enabled||!a.tokens||(n+=a.tokens.value,t=!0,a.tokens.kind==="estimate"&&(s="estimate"));return t?{kind:s,value:n}:void 0}function Q(e){let n=0,t=!1,s="exact";for(const a of L){const i=Y(e.slots[a]??[]);i&&(n+=i.value,t=!0,i.kind==="estimate"&&(s="estimate"))}return t?{kind:s,value:n}:void 0}function _e(e){return e==="full"?"完整 · 插件 API":e==="push"?"推送 · hooks":"未接线"}function Oe(e){return`${e.slice(0,16).replace("T"," ")} UTC`}function Z(e){const n=[];return n.push(e.nativePath?`文件:${e.nativePath}`:`无文件实体(source: ${e.source})`),e.__source_note&&n.push(e.__source_note),(e.tokens?.kind==="estimate"||e.onTriggerTokens?.kind==="estimate")&&n.push("估算 = 字节 ÷ 3.5,故标「约」"),e.tokens?.kind==="exact"&&n.push(`精确值(harness 自报 usage):${e.tokens.value.toLocaleString("en-US")} tokens`),n.join(`
2
+ `)}function Le(e){const n=[],t=[];for(const s of L){const a=(e.slots[s]??[]).filter(i=>i.toggleable);a.length>0?n.push({slot:s,items:a}):t.push(s)}return{groups:n,slotsWithoutToggle:t}}function Ae(e){return e.tokens?e.enabled?`关掉可省 ${k(e.tokens)}`:`开启将 +${k(e.tokens)}`:""}function N(e){return e===""?0:e.split(`
3
+ `).length-(e.endsWith(`
4
+ `)?1:0)}function Ne(e){return e==="create"?"新建":e==="delete"?"删除":"修改"}function xe(e){return e.kind==="create"?`${N(e.after??"")} 行`:e.kind==="delete"?`原 ${N(e.before??"")} 行`:`${N(e.before??"")} 行 → ${N(e.after??"")} 行`}function je(e){return e==="immediate"?"立即生效 —— 该机制被 harness 实时监听,当前 session 也会更新":e==="reconnect"?"重连后生效 —— 需重启该 harness(或重连其 MCP server)才加载":"下个 session 生效 —— 正在进行的对话不受影响,新开会话才加载新配置"}function z(e,n){return`${n?"打开":"关闭"} ${e}`}function Me(e){const n=e.action.itemId.split(":");return n.length>=4?n.slice(3).join(":"):e.action.itemId}const Pe=["cc","codex","dsh"],Ce=["immediate","next_session","reconnect"],Re=["create","modify","delete"];function f(e){throw new Error(`plan 载荷不合合同,拒绝渲染:${e}`)}function Ie(e,n){(typeof e!="object"||e===null)&&f(`writes[${n}] 不是对象`);const t=e;(typeof t.file!="string"||t.file==="")&&f(`writes[${n}].file 不是非空字符串`),Re.includes(t.kind)||f(`writes[${n}].kind 未知值 ${JSON.stringify(t.kind)}`),t.kind!=="create"&&typeof t.before!="string"&&f(`writes[${n}].before 缺失(kind=${String(t.kind)} 必须携带变更前完整内容)`),t.kind!=="delete"&&typeof t.after!="string"&&f(`writes[${n}].after 缺失(kind=${String(t.kind)} 必须携带变更后完整内容)`)}function De(e,n){(typeof e!="object"||e===null)&&f(`references[${n}] 不是对象`);const t=e;(typeof t.file!="string"||t.file==="")&&f(`references[${n}].file 不是非空字符串`),(typeof t.line!="number"||!Number.isInteger(t.line)||t.line<1)&&f(`references[${n}].line 不是 1-based 行号`),typeof t.matchedName!="string"&&f(`references[${n}].matchedName 不是字符串`),typeof t.context!="string"&&f(`references[${n}].context 不是字符串`)}function ee(e){(typeof e!="object"||e===null)&&f("不是对象");const n=e;Pe.includes(n.harness)||f(`harness 未知值 ${JSON.stringify(n.harness)}`),(typeof n.action!="object"||n.action===null)&&f("action 不是对象");const t=n.action;return t.kind!=="toggle"&&f(`action.kind 未知值 ${JSON.stringify(t.kind)}(v1 仅 toggle 走 plan)`),(typeof t.itemId!="string"||t.itemId==="")&&f("action.itemId 不是非空字符串"),typeof t.enable!="boolean"&&f("action.enable 不是布尔值"),Array.isArray(n.writes)||f("writes 不是数组"),n.writes.forEach(Ie),(typeof n.backupDir!="string"||n.backupDir==="")&&f("backupDir 不是非空字符串"),Array.isArray(n.references)||f("references 不是数组"),n.references.forEach(De),Ce.includes(n.effectiveAt)||f(`effectiveAt 未知值 ${JSON.stringify(n.effectiveAt)}`),e}function He(e){if(typeof e!="object"||e===null)throw new Error("apply 响应不合合同:不是对象");const n=e;if(n.ok===!0){if(typeof n.backupDir!="string"||n.backupDir==="")throw new Error("apply 响应不合合同:ok=true 但缺 backupDir");return{ok:!0,backupDir:n.backupDir}}if(n.ok===!1){if(typeof n.error!="string"||n.error==="")throw new Error("apply 响应不合合同:ok=false 但缺 error 原文");return{ok:!1,error:n.error}}throw new Error("apply 响应不合合同:ok 不是布尔值")}function Ue(e,n){return n.includes("fixtures 模式只读")?{phase:"demo",title:e}:{phase:"error",title:e,message:n}}function Fe(e){return new URLSearchParams(e).get("island")==="1"}function qe(e,n,t=null,s=null){const a=e?"#island=open":`#island=pill:${Be(n??{x:0,y:0,w:0,h:0})}`,i=t?`;focus:${t.target},${t.seq}`:"",c=s?`;waiting:${Math.round(s.sinceMs)},${encodeURIComponent(s.message)}`:"";return`${a}${i}${c}`}function Be(e){return`${Math.round(e.x)},${Math.round(e.y)},${Math.round(e.w)},${Math.round(e.h)}`}function Ke(e){return e.island&&e.open&&!e.inPanel&&!e.modalOpen}const We="https://billing.stripe.com/p/login/14AcMZ8Z147F4MmaTKcMM00",Je=["none","invalid","active","grace","expired"];function ne(e){if(typeof e!="object"||e===null)return null;const n=e;return!Je.includes(n.state)||typeof n.detail!="string"?null:{state:n.state,detail:n.detail}}function te(e){return e&&(e.state==="grace"||e.state==="expired")?e.state:null}function Ve(e){return e?`license 已导入 —— ${e.detail}`:"license 已导入(新状态未知,刷新本页查看)"}function Ge(e){if(e.length!==1)return`一次拖入 1 个 license.json 文件(收到 ${e.length} 个)`;const n=e[0]??"";return n.toLowerCase().endsWith(".json")?null:`只认 .json 文件(收到 ${n})`}const Xe=50,Ye=15e3,Qe={SessionStart:"会话开始",SessionEnd:"会话结束",UserPromptSubmit:"提交提示",PreToolUse:"工具调用开始",PostToolUse:"工具调用完成",PostToolBatch:"工具批次完成",Stop:"回合结束",SubagentStart:"子代理启动",SubagentStop:"子代理结束",PreCompact:"压缩开始",PostCompact:"压缩完成",ConfigChange:"配置变更",InstructionsLoaded:"指令加载",Notification:"通知",PermissionRequest:"权限请求"};function se(e){return Qe[e]??e}function Ze(e,n,t=Xe){const s=[...e,n];return s.length>t?s.slice(s.length-t):s}function ze(e){return e.slice(11,19)}function en(e){const n={cc:0,codex:0,dsh:0};for(const{event:t}of e)n[t.harness]+=1;return n}function nn(e,n){const t=[];for(const{event:s}of e){if(s.harness!==n)continue;const{hook_event_name:a,session_id:i,tool_name:c}=s.payload,u=typeof i=="string"?i:"";if(a==="PreToolUse"&&typeof c=="string")t.push({session:u,tool:c});else if(a==="PostToolUse"&&typeof c=="string")for(let l=t.length-1;l>=0;l--){const p=t[l];if(p&&p.session===u&&p.tool===c){t.splice(l,1);break}}else if(a==="Stop"||a==="SessionEnd")for(let l=t.length-1;l>=0;l--)t[l]?.session===u&&t.splice(l,1)}return t[t.length-1]?.tool}function ae(e,n,t,s){const a=e.filter(l=>l.event.harness===n),i=a[a.length-1];if(!i)return{active:!1,label:t==="none"?"未接线":"等待事件"};const c=Date.parse(i.event.received_at);if(!Number.isFinite(c)||s-c>Ye)return{active:!1,label:"静默"};const u=nn(e,n);return{active:!0,label:u?`${u} 运行中`:se(i.event.payload.hook_event_name)}}function tn(e){return e.replaying?"演示重放中 · MOCK":e.connection==="open"?"实况已连接":e.connection==="retrying"?"连接断开 · 自动重连中":"连接中…"}function sn(e){if(typeof e!="object"||e===null)return null;const n=e;return typeof n.harness!="string"||!Array.isArray(n.todos)||!Array.isArray(n.traces)||!Array.isArray(n.turnUsages)||typeof n.updatedAt!="string"?null:e}function an(e){if(typeof e!="object"||e===null)return null;const n=e;if(typeof n.harness!="string"||!Array.isArray(n.sessions))return null;for(const t of n.sessions){if(typeof t!="object"||t===null)return null;const s=t;if(typeof s.id!="string"||typeof s.label!="string"||typeof s.lastActivity!="string"||typeof s.waiting!="boolean"||typeof s.ended!="boolean")return null}if(typeof n.states!="object"||n.states===null||Array.isArray(n.states))return null;for(const t of Object.values(n.states))if(sn(t)===null)return null;return e}function C(e,n){return!e||e.sessions.length===0?null:n!==void 0&&e.states[n]!==void 0?n:e.sessions[0]?.id??null}function ie(e){if(!e)return null;for(const n of e.sessions){if(!n.waiting)continue;const t=e.states[n.id]?.waiting;if(t)return{label:n.label,message:t.message,since:t.since}}return null}function oe(e){return e?e.todos.length>0||e.traces.length>0||e.turnUsages.length>0||e.currentAction!==null||e.waiting!==null:!1}const on={completed:"✓",in_progress:"▶",pending:"○"};function rn(e){const n=e.todos.find(t=>t.status==="in_progress");return n?n.activeForm??n.content:e.lastPrompt}function re(e){return e?e.kind==="tool"?e.detail===""?e.toolName:`${e.toolName} · ${e.detail}`:ce(e.name):null}const cn={SessionStart:"会话开始",SessionEnd:"会话结束",UserPromptSubmit:"收到新提示",PreToolUse:"工具调用开始",PostToolUse:"工具调用完成",Stop:"回合结束",SubagentStart:"子代理启动",SubagentStop:"子代理结束",PreCompact:"压缩开始",PostCompact:"压缩完成"};function ce(e){return cn[e]??e}function le(e){return e?e.turnUsages[e.turnUsages.length-1]??null:null}function de(e){return e<.005?"<$0.01":`$${e.toFixed(2)}`}function x(e){const n=`${y(e.contextTotal.value)} tok`;return e.costUsd===void 0?n:`${n} ≈ ${de(e.costUsd)}`}function R(e){if(e<1e3)return"<1s";const n=Math.round(e/1e3);return n<60?`${n}s`:`${Math.floor(n/60)}m${n%60===0?"":` ${n%60}s`}`}function ln(e,n){return e.agents.length>0?"子 agent":n&&n.nodes.length>0?"主 agent":null}function dn(e){return!e||e.todos.length===0?null:`${e.todos.filter(t=>t.status==="completed").length}/${e.todos.length}`}const un=6e4;function pn(e,n,t,s,a){const i=le(e),c=s!==void 0?s:e?.waiting?{label:"",message:e.waiting.message,since:e.waiting.since}:null;if(c){const u=c.label===""?"":`(${c.label})`,l=Date.parse(c.since),p=a!==void 0&&Number.isFinite(l)?a-l:Number.NaN,v=Number.isFinite(p)&&p>un?` · 已等 ${R(p)}`:"";return{mode:"waiting",label:`等你${u}:${c.message}${v}`,tok:i?x(i):t}}return n.active?{mode:"working",label:(e?re(e.currentAction):null)??n.label,tok:i?`本轮 ${x(i)}`:t}:{mode:"idle",label:n.label,tok:i?`上轮 ${x(i)}`:t}}function ue(e,n){const t=n[e.id];return t!==void 0?t:e.status==="in_progress"}const fn="https://api.getwingman.dev/samples",vn=256*1024;function mn(e){return e.__mock_note?[]:JSON.stringify(e,null,2).split(`
5
+ `)}function $n(e){return e.filter(n=>!n.mock).map(n=>JSON.stringify(n.event))}function hn(e,n){return!Number.isInteger(n)||n<0||n>=e.length?[...e]:e.filter((t,s)=>s!==n)}function gn(e){return e.join(`
6
+ `)}function pe(e,n){return JSON.stringify({kind:e,sample:gn(n)})}function fe(e,n){return new TextEncoder().encode(pe(e,n)).length}function ve(e,n){return fe(e,n)>vn}function bn(e,n){const t=(fe(e,n)/1024).toFixed(1);return`共 ${n.length} 行 · 约 ${t} KB / 上限 256 KB`}function wn(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}const r=wn;function F(e){return`<div class="empty-card">
7
+ <div class="empty-title">未检测到 harness</div>
8
+ ${e?`<div class="empty-detail">${r(e)}</div>`:'<div class="empty-detail">/api/anatomy 返回了空数组</div>'}
9
+ <div class="empty-hint">先运行 <code>npx harness-wingman serve</code>,面板从 GET /api/anatomy 读取解剖数据。</div>
10
+ </div>`}function yn(e,n){const t=n.mode==="waiting"?" wait":n.mode==="working"?" live":" quiet",s=n.mode==="waiting"?"有未决的权限请求/通知(PermissionRequest/Notification 事件);点击展开处理":n.mode==="working"?"有活跃 session(最近 15s 内收到 hook 事件);点击展开面板":"点击展开面板";return`<div class="pill${n.mode==="waiting"?" waiting":""}" data-act="toggle" data-tip="${r(s)}">
11
+ <span class="dot${t}"></span>
12
+ <span class="t1">${r(T[e.harness])}</span>
13
+ <span class="t2">${r(n.label)}</span>
14
+ ${n.tok===""?"":`<span class="tok">${r(n.tok)}</span>`}
15
+ <span class="chev">▾</span>
16
+ </div>`}function kn(e,n,t){return`<div class="tabs">${e.map(a=>{const i=a.harness===n,c=t[a.harness],u=!i&&c>0?`<span class="tabnum" data-tip="实况流内 ${T[a.harness]} 事件 ${c} 条">${c}</span>`:"";return`<button class="tab${i?" act":""}" data-tab="${a.harness}">${r(T[a.harness])}${u}</button>`}).join("")}</div>`}function Sn(e){const n=L.map(l=>({slot:l,total:Y(e.slots[l]??[])})),t=n.reduce((l,p)=>l+(p.total?.value??0),0),s=Q(e),a=s?`常驻合计 <b data-tip="合计 = 各槽位启用条目的常驻 token 之和;${s.kind==="estimate"?"含估算值,故标「约」":"全部为 harness 自报精确值"}">${r(k(s))} token</b>`:"本次扫描未采到 token 数值(无可估算的字节来源)",i=t>0?n.filter(l=>l.total&&l.total.value>0).map(l=>{const p=(l.total?.value??0)/t*100,v=`${E[l.slot]} · ${k(l.total)}`;return`<div class="seg" style="width:${p.toFixed(1)}%;background:${M[l.slot]}" data-tip="${r(v)}"></div>`}).join(""):"",c=i?`<div class="stack">${i}</div>`:"",u=n.map(({slot:l,total:p})=>{const v=!p,g=e.slots[l]??[],w=v?`${E[l]}:本次扫描该槽位无带 token 来源的启用条目`:`${E[l]} 启用条目常驻合计(逐条来源见下方明细)`;return`<div class="lrow${v?" na":""}" data-tip="${r(w)}">
17
+ <span class="sw" style="background:${v?"var(--grid)":M[l]}"></span>${E[l]}
18
+ <span class="nat">${r(K[e.harness][l])}</span>
19
+ <span class="v">${r(k(p))}</span>
20
+ <span class="cnt">${g.length} 项</span>
21
+ </div>`}).join("");return`<div class="sec">
22
+ <div class="label">CONTEXT 组成<span class="labr"><button class="sharelink" data-act="share-config" data-tip="打开预览弹层:先看到将发送的完整内容,可逐行删,点「发送」才会发出;关闭 = 什么都不发">分享配置样本</button></span></div>
23
+ <div class="headline">${a}</div>
24
+ ${c}
25
+ <div class="legend">${u}</div>
26
+ </div>`}function Tn(e){const n=e.enabled?"":'<span class="statechip off" data-tip="该条目当前被关闭;可开关条目在下方 CONFIGURE 区一键打开">已关闭</span>',t=e.nativePath?"":'<span class="statechip nofile" data-tip="无文件实体:该条目不来自可读文件(如内置注入 / boot 树行)">无文件实体</span>';return`<div class="crow${e.enabled?"":" offrow"}" data-tip="${r(Z(e))}">
27
+ <span class="cname">${r(e.nativeName)}</span>
28
+ <span class="chip">${r(e.source)}</span>
29
+ ${n}${t}
30
+ <span class="ctok">${r(Ee(e))}</span>
31
+ </div>`}function En(e){return`<div class="sec">
32
+ <div class="label">六槽位明细 · hover 查看来源</div>
33
+ ${L.map(t=>{const s=e.slots[t]??[],a=s.length?s.map(i=>Tn(i)).join(""):'<div class="crow emptyrow">本次扫描无条目(该槽位在此 harness 无可读来源)</div>';return`<div class="grp">
34
+ <div class="gtitle"><span class="gdot" style="background:${M[t]}"></span>${E[t]}
35
+ <span class="nat">${r(K[e.harness][t])}</span>
36
+ </div>
37
+ ${a}
38
+ </div>`}).join("")}
39
+ </div>`}function _n(e){const n=Ae(e),t=e.enabled?"":'<span class="statechip off">关闭中</span>';return`<div class="crow${e.enabled?"":" offrow"}" data-tip="${r(Z(e))}">
40
+ <span class="cname">${r(e.nativeName)}</span>
41
+ <span class="chip">${r(e.source)}</span>
42
+ ${t}
43
+ <span class="ctok">${r(n)}</span>
44
+ <span class="cright"><button class="toggle${e.enabled?" on":""}" data-act="tgl" data-id="${r(e.id)}" role="switch" aria-checked="${e.enabled}" aria-label="${r(`${e.enabled?"关闭":"打开"} ${e.nativeName}`)}" data-tip="点击预览改动 —— 先 plan 后 apply:确认前不写任何文件;写前自动备份,可 byte-identical 回退"></button></span>
45
+ </div>`}function On(e){const n=Le(e);let t;if(n.groups.length===0)t='<div class="crow emptyrow">该 harness 本次扫描没有可在面板开关的条目 —— 开关范围仅限各家原生支持的机制,其余条目请在源文件里改</div>';else if(t=n.groups.map(s=>{const a=s.items.map(c=>_n(c)).join(""),i=e.harness==="codex"&&s.slot==="tools"?'<div class="cfgnote" data-tip="PLAN §4b 实测:config.toml 的 enabled_tools/disabled_tools 按单个工具开关;CC 只能整 server 开关,dsh 只能整插件开关">Codex 独有:enabled_tools / disabled_tools 可精确到单个工具 —— 三家里粒度最细</div>':"";return`<div class="grp">
46
+ <div class="gtitle"><span class="gdot" style="background:${M[s.slot]}"></span>${E[s.slot]}
47
+ <span class="nat">${r(K[e.harness][s.slot])}</span>
48
+ </div>
49
+ ${a}${i}
50
+ </div>`}).join(""),n.slotsWithoutToggle.length>0){const s=n.slotsWithoutToggle.map(a=>E[a]).join(" · ");t+=`<div class="cfgnote">其余槽位(${s})本次扫描无可开关条目 —— 开关范围仅限各家原生支持的机制</div>`}return`<div class="sec">
51
+ <div class="label">CONFIGURE</div>
52
+ ${t}
53
+ </div>`}function Ln(e,n){const t=e.writes.length?e.writes.map(i=>`<div class="mfile"><span class="mkind ${i.kind}">${Ne(i.kind)}</span><code class="mpath">${r(i.file)}</code><span class="mlines">${r(xe(i))}</span></div>`).join(""):'<div class="mtext muted">plan 未包含任何文件变更</div>',s=e.references.length?e.references.map(i=>`<div class="mref"><code class="mpath">${r(`${i.file}:${i.line}`)}</code><span class="mctx">${r(i.context)}</span></div>`).join(""):'<div class="mtext muted">引用语料里没有别处提到它</div>';return`${n?`<div class="mnote">${r(n)}</div>`:""}
54
+ <div class="msec"><div class="mlabel">将写入的文件 — 确认前一个字节都不动</div>${t}</div>
55
+ <div class="msec"><div class="mlabel">引用命中 ${e.references.length} 处 — 这些地方提到了它;只提醒,不修复</div>${s}</div>
56
+ <div class="msec"><div class="mlabel">生效时机</div><div class="mtext">${r(je(e.effectiveAt))}</div></div>
57
+ <div class="msec"><div class="mlabel">备份</div><div class="mtext">写入前,原件先备份到 <code>${r(e.backupDir)}</code>;回退 = 原样拷回(byte-identical)</div></div>
58
+ <div class="mact">
59
+ <button class="btn ghost" data-act="plan-cancel">取消</button>
60
+ <button class="btn primary" data-act="plan-confirm">确认写入</button>
61
+ </div>`}function An(e){const n=e.phase==="confirm"&&e.mockNote?`<span class="mockchip" data-tip="${r(e.mockNote)}">MOCK</span>`:"";let t;e.phase==="planning"?t='<div class="mtext muted">正在生成 plan…(纯读:不写盘、不建备份目录)</div>':e.phase==="applying"?t='<div class="mtext muted">正在写入…(原件已先备份到 plan 里的备份目录)</div>':e.phase==="confirm"?t=Ln(e.plan,e.mockNote):e.phase==="applied"?t=`<div class="mtext">已写入,面板数据已重拉刷新。</div>
62
+ <div class="mtext">备份:<code>${r(e.backupDir)}</code>(回退 = 原样拷回)</div>
63
+ <div class="mact"><button class="btn primary" data-act="plan-cancel">关闭</button></div>`:e.phase==="demo"?t=`<div class="mtext">演示模式:动作只到 plan 为止 —— 当前数据源只读(fixtures / dev 演示),不写任何真实文件。</div>
64
+ <div class="mact"><button class="btn primary" data-act="plan-cancel">知道了</button></div>`:t=`<div class="merror">${r(e.message)}</div>
65
+ <div class="mtext muted">常见原因:文件在 plan 之后被外部改过(关闭后重新点开关生成新 plan)、itemId 不存在、或目标格式未知被拒写(不猜,如实拒绝)。</div>
66
+ <div class="mact"><button class="btn ghost" data-act="plan-cancel">关闭</button></div>`;const s=e.phase!=="applying";return`<div class="overlay">
67
+ <div class="scrim"${s?' data-act="plan-cancel"':""}></div>
68
+ <div class="modal" role="dialog" aria-modal="true" aria-label="${r(e.title)}">
69
+ <div class="mhead"><span class="mtitle">${r(e.title)}</span>${n}${s?'<button class="mclose" data-act="plan-cancel" aria-label="关闭弹窗">✕</button>':""}</div>
70
+ ${t}
71
+ </div>
72
+ </div>`}function Nn(e){const n=te(e??null);if(!n||!e)return"";const t=n==="grace"?"订阅已到期,宽限期内 · 点此续期":"订阅已到期,写动作已锁";return`<div class="licrow"><button class="licbar ${n}" data-act="lic-open" data-tip="${r(e.detail)}">${r(t)}</button>${me("licimport")}</div>`}function me(e){return`<button class="${e}" data-act="lic-import" data-tip="选择激活页下载的 license.json 导入(写入 ~/.wingman/license.json,旧文件自动备份);也可把文件直接拖进本面板">导入 license</button>`}function xn(e){return`<div class="overlay">
73
+ <div class="scrim" data-act="import-close"></div>
74
+ <div class="modal" role="dialog" aria-modal="true" aria-label="导入 license">
75
+ <div class="mhead"><span class="mtitle">导入 license</span><button class="mclose" data-act="import-close" aria-label="关闭弹窗">✕</button></div>
76
+ ${e.ok?`<div class="mtext">${r(e.message)}</div>`:`<div class="merror">${r(e.message)}</div>`}
77
+ <div class="mact"><button class="btn primary" data-act="import-close">关闭</button></div>
78
+ </div>
79
+ </div>`}function jn(e,n=We){const t=n===""?"取消订阅或更换付款方式在 Stripe 侧完成,入口在订阅邮箱收到的 Stripe 邮件里。":`<a href="${r(n)}" target="_blank" rel="noreferrer">管理订阅(取消/换卡/发票)</a>`;return`<div class="overlay">
80
+ <div class="scrim" data-act="lic-close"></div>
81
+ <div class="modal" role="dialog" aria-modal="true" aria-label="续期">
82
+ <div class="mhead"><span class="mtitle">续期</span><button class="mclose" data-act="lic-close" aria-label="关闭弹窗">✕</button></div>
83
+ <div class="msec"><div class="mlabel">当前状态</div><div class="mtext">${r(e.detail)}</div></div>
84
+ <div class="msec"><div class="mlabel">续期</div><div class="mtext">联网运行 <code>npx harness-wingman license refresh</code>:订阅仍有效即换发新 license(写回 <code>~/.wingman/license.json</code>),刷新本页生效;停订会被拒。</div></div>
85
+ <div class="msec"><div class="mlabel">订阅管理</div><div class="mtext">${t}</div></div>
86
+ <div class="mact"><button class="btn primary" data-act="lic-close">关闭</button></div>
87
+ </div>
88
+ </div>`}function Mn(e){let n;if(e.phase==="preview"){const t=e.lines.length?e.lines.map((c,u)=>`<div class="shline"><button class="shdel" data-act="share-del" data-i="${u}" aria-label="删除第 ${u+1} 行" data-tip="删掉这一行(只影响本次发送)">✕</button><span class="shtext">${r(c)}</span></div>`).join(""):'<div class="mtext muted">没有可发送的内容(MOCK 演示数据不外发;删光了也一样不发)</div>',s=ve(e.kind,e.lines),a=s?'<div class="merror">样本超过 256 KB 上限,删减后才能发送</div>':"",i=e.lines.length===0||s?" disabled":"";n=`<div class="mtext">分享样本帮助 Wingman 适配更多真实环境。下面就是将要发送的全部内容:逐行检查,不想发的行点 ✕ 删掉;点「发送」才会发出,关闭则什么都不发。样本不含任何账号或设备标识,服务端也不记录 IP。</div>
89
+ <div class="msec"><div class="mlabel">将发送的内容 · ${r(bn(e.kind,e.lines))}</div>
90
+ <div class="sharebox">${t}</div>${a}</div>
91
+ <div class="mact">
92
+ <button class="btn ghost" data-act="share-close">关闭(不发送)</button>
93
+ <button class="btn primary" data-act="share-send"${i}>发送</button>
94
+ </div>`}else e.phase==="sending"?n=`<div class="mtext muted">发送中…(POST ${r("api.getwingman.dev/samples")})</div>`:e.phase==="sent"?n=`<div class="mtext">已发送。样本只用于适配真实环境,90 天后在服务端自动过期。</div>
95
+ <div class="mact"><button class="btn primary" data-act="share-close">关闭</button></div>`:n=`<div class="merror">${r(e.message??"发送失败")}</div>
96
+ <div class="mtext muted">没有发出任何内容;稍后可重新分享。</div>
97
+ <div class="mact"><button class="btn ghost" data-act="share-close">关闭</button></div>`;return`<div class="overlay">
98
+ <div class="scrim" data-act="share-close"></div>
99
+ <div class="modal" role="dialog" aria-modal="true" aria-label="${r(e.title)}">
100
+ <div class="mhead"><span class="mtitle">${r(e.title)}</span><button class="mclose" data-act="share-close" aria-label="关闭弹窗">✕</button></div>
101
+ ${n}
102
+ </div>
103
+ </div>`}function Pn(e,n){const{event:t}=e,s=t.payload.hook_event_name,a=typeof t.payload.tool_name=="string"?t.payload.tool_name:"",i=typeof t.payload.session_id=="string"?t.payload.session_id:"(无 session_id)",c=[`事件:${s}(hook_event_name 原文)`,`时间 = receiver 收到时刻 received_at:${t.received_at}`,`session:${i}`];return e.mock&&c.push("MOCK:dev 演示重放注入,非真实 harness 推送(增补条款 10)"),`<div class="lvrow${t.harness===n?" own":" other"}" data-tip="${r(c.join(`
104
+ `))}">
105
+ <span class="lvtime">${r(ze(t.received_at))}</span>
106
+ <span class="lvname">${r(se(s))}</span>
107
+ ${a?`<span class="chip">${r(a)}</span>`:""}
108
+ ${e.mock?'<span class="mockchip">MOCK</span>':""}
109
+ <span class="lvharness">${r(t.harness)}</span>
110
+ </div>`}function Cn(e,n){const t=e.capabilities.live,s=n.entries.filter(c=>c.event.harness===e.harness);let a;n.entries.length>0?(a=`<div class="feed">${[...n.entries].reverse().map(u=>Pn(u,e.harness)).join("")}</div>`,t==="none"&&s.length===0&&(a=`<div class="livenone">○ ${r(T[e.harness])} 未接线 —— 运行 <code>npx harness-wingman hook install ${r(e.harness)}</code> 接线后,其 hook 事件才会出现在下方流里(当前流内是别家事件)</div>`+a)):t==="none"?a=`<div class="livenone">○ 未接线 —— 运行 <code>npx harness-wingman hook install ${r(e.harness)}</code> 把该 harness 的 hooks 指到本面板(幂等、可 uninstall 恢复);接线前这里不显示任何数据</div>`:a=`<div class="livenone">○ 已接线(${t==="full"?"完整":"推送"}),等待事件 —— 在 ${r(T[e.harness])} 里跑一轮对话即可看到实况流</div>`;const i=n.connection==="retrying"?`<button class="demobtn" data-act="demo" ${n.replaying?"disabled":""} data-tip="EventSource 连不上(dev 模式无 SSE 端点)时的本地演示:以 200ms 间隔重放 fixtures 的 cc 事件序列,注入事件全部带 MOCK 徽章">重放 fixtures 演示 · MOCK</button>`:"";return`<div class="sec">
111
+ <div class="label">实况 <span class="tier${t==="none"?" none":""}">${r(_e(t))}</span>
112
+ <span class="conn ${n.replaying?"replay":n.connection}" data-tip="GET /api/events(SSE)连接状态;断线由 EventSource 自动重连">${r(tn(n))}</span>
113
+ <span class="labr">${i}<button class="sharelink" data-act="share-events" data-tip="打开预览弹层:先看到将发送的完整事件流,可逐行删,点「发送」才会发出;关闭 = 什么都不发">分享事件样本</button></span>
114
+ </div>
115
+ ${a}
116
+ </div>`}const $e={view:"tasks",narrative:null,sessions:[],selectedSession:null,sessionSeen:{},suggHidden:{},island:!1,todoOpen:{},nodeOpen:{},rawOpen:!1};function Rn(e,n){return e.sessions.length<2?"":`<div class="schips">${e.sessions.map(s=>{const a=s.id===e.selectedSession,i=e.sessionSeen[`${n}:${s.id}`]??"",c=!a&&s.lastActivity>i,u=[`session:${s.id}`,`最近活动:${s.lastActivity||"(未观察到)"}`,s.ended?"已结束(SessionEnd,或 Stop 后 10 分钟无事件)":"进行中",s.waiting?"等你:有未决的权限请求/通知":"",c?"有新活动(上次查看后)":""].filter(p=>p!=="").join(`
117
+ `),l=s.waiting?'<span class="sdot wait"></span>':c?'<span class="sdot"></span>':"";return`<button class="schip${a?" act":""}${s.ended?" ended":""}" data-act="session" data-id="${r(s.id)}" data-tip="${r(u)}">${l}${r(s.label)}</button>`}).join("")}</div>`}function In(e,n){const t=e.narrative?.suggestions??[];if(t.length===0)return"";const s=e.selectedSession??"";return t.filter(a=>e.suggHidden[`${n}:${s}:${a.id}`]!==!0).map(a=>{const i=a.action?`<button class="sugbtn" data-act="sugg-plan" data-item="${r(a.action.itemId)}" data-enable="${a.action.enable?"1":"0"}" data-tip="打开 M3 plan 弹窗:先看方案(写哪些文件/生效时机/备份),确认才写入">看方案</button>`:"";return`<div class="sugg r-${a.rule}" data-tip="${r(a.source)}">
118
+ <span class="sugtext">${r(a.text)}</span>${i}<button class="sugdismiss" data-act="sugg-dismiss" data-sugg="${r(a.id)}" data-tip="本 session 不再提示这条(内存态,重开面板恢复)">不再提示</button>
119
+ </div>`}).join("")}function Dn(e){const n=rn(e),s=e.todos.some(g=>g.status==="in_progress")?"第一条进行中 todo(来源:transcript 的 TaskCreate/TaskUpdate 事件)":"最新用户提示摘要(本会话暂无进行中 todo)",a=re(e.currentAction),i=le(e),c=e.__mock_note?`<span class="mockchip" data-tip="${r(e.__mock_note)}">MOCK</span>`:"",u=e.waiting?`<div class="nwait" data-tip="${r(`未决 ${e.waiting.kind==="permission"?"PermissionRequest":"Notification"} 事件,发生于 ${e.waiting.since}`)}">等你:${r(e.waiting.message)}</div>`:"",l=i?`<span data-tip="本轮 context = usage 三字段之和(harness 自报,精确)${i.costUsd===void 0?"":";$ 按内置价目表换算(约)"}">本轮 ${r(x(i))}</span>`:"",p=a!==null?`<div class="ndoing">正在:<b>${r(a)}</b>${l===""?"":` · ${l}`}</div>`:l===""?"":`<div class="ndoing">消耗:${l}</div>`,v=e.agentQuote?`<div class="nquote" data-tip="agent 自述原话(transcript assistant 文本块);引用,不生成">「${r(e.agentQuote)}」</div>`:"";return`<div class="now">
120
+ ${u}
121
+ <div class="ntask" data-tip="${r(s)}">${n===null?'<span class="muted">(本会话暂无任务信息)</span>':r(n)}${c}</div>
122
+ ${p}
123
+ ${v}
124
+ </div>`}function Hn(e){const n=dn(e.narrative),t=e.narrative?.turnUsages.length??0,s=(a,i,c)=>`<button class="vtab${e.view===a?" act":""}" data-act="view" data-view="${a}">${i}${c===null?"":` <span class="vcnt">${r(c)}</span>`}</button>`;return`<div class="vtabs">
125
+ ${s("tasks","任务",n)}
126
+ ${s("control","控制",null)}
127
+ ${s("billing","账单",t>0?`${t} 轮`:null)}
128
+ </div>`}function Un(e){const n=e.ts!==void 0&&e.endTs!==void 0?R(Date.parse(e.endTs)-Date.parse(e.ts)):e.endTs===void 0?"…":"",t=e.estTokens===void 0?"":` · 约 ${y(e.estTokens)}`,s=e.isError&&e.errorBrief!==void 0?`<span class="failchip">${r(e.errorBrief)}</span>`:"",a=e.agentId!==void 0?'<span class="ag">子 agent</span>':"";return`<div class="trs${e.isError?" bad":""}" data-tip="${r(`工具:${e.toolName}${e.ts===void 0?"":`
129
+ 开始:${e.ts}`}${e.estTokens===void 0?"":`
130
+ 约 token = 工具输入+结果字节 ÷ 3.5(估算)`}`)}">
131
+ <span class="cmd">${r(e.toolName)}</span>
132
+ ${e.detail===""?"":`<span class="sdetail">${r(e.detail)}</span>`}
133
+ ${s}${a}
134
+ <span class="dur">${r(n)}${r(t)}</span>
135
+ </div>`}function Fn(e){const n=e.wait?.resolvedBy!==void 0,t=e.durationMs===void 0?"":`等了 ${R(e.durationMs)}`,s=n?` · ${t===""?"":`${t} → `}${ce(e.wait?.resolvedBy??"")}`:" · 等待中…",a=[`${e.wait?.kind==="permission"?"PermissionRequest":"Notification"} 事件起止(hook 层观察)`,e.startTs===void 0?"":`开始:${e.startTs}`,n?`解除:${e.endTs??""}(事件 ${e.wait?.resolvedBy??""})`:"尚未解除"].filter(i=>i!=="").join(`
136
+ `);return`<div class="tr waitrow" data-tip="${r(a)}">
137
+ <span class="ic"></span>
138
+ <span class="trlabel">⏸ ${r(e.label)}:${r(e.wait?.message??"")}${r(s)}</span>
139
+ </div>`}function qn(e,n,t){if(e.phase==="wait")return Fn(e);const s=e.steps.some(p=>p.endTs===void 0),a=e.durationMs===void 0?s?"…":"":R(e.durationMs),i=e.estTokens===void 0?"":` · 约 ${y(e.estTokens)}`,c=e.steps.find(p=>p.isError&&p.errorBrief!==void 0),u=e.failed&&c!==void 0&&!t?`<span class="failchip">${r(c.errorBrief??"")}</span>`:"",l=t?e.steps.map(p=>Un(p)).join(""):"";return`<div class="tr${e.failed?" bad":s?" run":""}" data-act="node" data-key="${r(n)}" data-tip="${r(`阶段归类:工具类型规则(规则表公开);点击${t?"收起":"展开"}步骤${e.estTokens===void 0?"":`
140
+ 约 token = 节点内步骤字节估算之和`}`)}">
141
+ <span class="ic"></span>
142
+ <span class="trlabel">${r(e.label)} · ${e.steps.length} 步</span>
143
+ ${u}
144
+ <span class="dur">${r(a)}${r(i)}</span>
145
+ </div>${l===""?"":`<div class="steps">${l}</div>`}`}function he(e,n,t){return`<div class="trace">${e.nodes.map((a,i)=>{const c=`${n}:${i}`;return qn(a,c,t[c]===!0)}).join("")}</div>`}function Bn(e,n){return""}function Kn(e,n,t,s){const a=ue(e,t.todoOpen)&&n!==void 0&&n.nodes.length>0,i=n!==void 0&&n.nodes.length>0,c=e.status==="in_progress"&&i?a?"▾":"▸":on[e.status],u=ln(e,n),l=u===null?"":`<span class="ag" data-tip="${r(u==="子 agent"?`步骤归属含子代理(${e.agents.join(", ")});归属 = 时间窗 + Agent 跨度推断,归不准会进「其他」`:"步骤归属:主 agent(时间窗推断)")}">${r(u)}</span>`;return`<div class="td${e.status==="completed"?" done":e.status==="in_progress"?" doing":" todo"}">
146
+ <div class="tdr"${i?` data-act="todo" data-id="${r(e.id)}"`:""}>
147
+ <span class="mk${e.status==="in_progress"?" doing":""}">${c}</span>
148
+ <span class="tdc">${e.status==="in_progress"?`<b>${r(e.content)}</b>`:r(e.content)}</span>
149
+ ${l}
150
+ ${Bn()}
151
+ </div>
152
+ ${a&&n?he(n,e.id,t.nodeOpen):""}
153
+ </div>`}function q(e,n){const t=n.traces.find(a=>a.todoId===null);if(!t||t.nodes.length===0)return"";const s=e.todoOpen.__other__!==!1;return`<div class="td doing other">
154
+ <div class="tdr" data-act="todo" data-id="__other__" data-tip="归不到具体 todo 的步骤进「其他」桶(无时间戳/无进行中 todo/多任务并行归不准);推断规则如实降级,不硬塞">
155
+ <span class="mk">${s?"▾":"▸"}</span>
156
+ <span class="tdc">其他(归不准的步骤)</span>
157
+ </div>
158
+ ${s?he(t,"__other__",e.nodeOpen):""}
159
+ </div>`}function Wn(e,n){return`<div class="sec first todolist">
160
+ <div class="cfgnote" data-tip="来源:assistant 文本里的 markdown 勾选清单(- [ ] / - [x]);会话没用工具清单时的降级展示,工具清单一旦出现立即让位">文本清单(agent 手写)</div>
161
+ ${(e.textTodos??[]).map(s=>`<div class="td${s.checked?" done":" todo"}">
162
+ <div class="tdr"><span class="mk">${s.checked?"✓":"○"}</span><span class="tdc">${r(s.content)}</span></div>
163
+ </div>`).join("")}${q(n,e)}
164
+ </div>`}function Jn(e,n,t){const s=t.narrative;let a="";s&&s.todos.length>0?a=`<div class="sec first todolist">${s.todos.map(u=>Kn(u,s.traces.find(l=>l.todoId===u.id),t,e.harness)).join("")}${q(t,s)}</div>`:s&&(s.textTodos?.length??0)>0?a=Wn(s,t):s&&oe(s)?a=`<div class="sec first">
165
+ <div class="cfgnote">${e.harness==="dsh"?"该 harness 不产生任务清单 —— 以下为执行轨迹":"本次会话未使用任务清单 —— 以下为执行轨迹"}</div>
166
+ ${q(t,s)}
167
+ </div>`:a=`<div class="sec first"><div class="livenone">○ 暂无叙事数据 —— 接线(<code>npx harness-wingman hook install ${r(e.harness)}</code>)并跑一轮对话后,这里按任务展示执行轨迹</div></div>`;const i=`<div class="sec rawsec">
168
+ <button class="rawhead" data-act="raw" data-tip="逐条 hook 事件原始流(0.2 的实况视图);默认折叠">${t.rawOpen?"▾":"▸"} 原始事件</button>
169
+ <div class="rawbody${t.rawOpen?" open":""}">${Cn(e,n)}</div>
170
+ </div>`;return a+i}function Vn(e,n){const t=n.narrative?.turnUsages??[],s=t.map(i=>Gn(i)).join("");return`<div class="sec first">
171
+ <div class="label">每轮消耗 · usage 三字段为 harness 自报精确值</div>
172
+ ${t.length>0?`<div class="ubox">${s}</div>`:'<div class="livenone">○ 本会话尚无 usage 数据 —— 接线并跑一轮对话后,这里逐轮列出精确消耗</div>'}
173
+ </div>
174
+ ${Sn(e)}
175
+ ${En(e)}`}function Gn(e){const n=e.ts===void 0?"":e.ts.slice(11,19),t=e.costUsd===void 0?"":`<span class="ucost">≈ ${r(de(e.costUsd))}</span>`,s=["usage 原文(harness 自报,精确):",`input ${e.inputTokens} · 缓存写 ${e.cacheCreationInputTokens} · 缓存读 ${e.cacheReadInputTokens} · output ${e.outputTokens}`,`本轮 context = 三字段之和 = ${e.contextTotal.value}`,e.costUsd===void 0?"$:该模型不在内置价目表,不换算(宁缺勿编)":"$ 按内置价目表换算(缓存读 ×0.1、缓存写 ×1.25/×2),标 ≈"].join(`
176
+ `);return`<div class="urow" data-tip="${r(s)}">
177
+ <span class="utime">${r(n)}</span>
178
+ ${e.model===void 0?"":`<span class="chip">${r(e.model)}</span>`}
179
+ <span class="unums">in ${r(y(e.inputTokens))} · 缓写 ${r(y(e.cacheCreationInputTokens))} · 缓读 ${r(y(e.cacheReadInputTokens))} · out ${r(y(e.outputTokens))}</span>
180
+ <span class="utot">Σ ${r(y(e.contextTotal.value))}</span>
181
+ ${t}
182
+ </div>`}function Xn(e,n,t,s=null,a=$e){const i=e.find(g=>g.harness===n)??e[0];if(!i)return`<div class="panel">${F()}</div>`;const c=i.__mock_note?`<span class="badge" data-tip="${r(i.__mock_note)}">MOCK 演示数据</span>`:"",u=i.harnessVersion?` ${i.harnessVersion}`:"",l=`${T[i.harness]}${u} · 扫描于 ${Oe(i.scannedAt)}`,p=a.narrative,v=p&&oe(p)?Dn(p):"";return`<div class="panel">
183
+ <div class="head">
184
+ <span class="dot"></span>
185
+ <span class="title">Wingman</span>
186
+ <span class="meta">${r(l)}</span>
187
+ ${c}
188
+ <button class="collapse" data-act="toggle">收起 ▴</button>
189
+ </div>
190
+ ${Nn(s)}${kn(e,i.harness,en(t.entries))}
191
+ ${Rn(a,i.harness)}
192
+ ${v}
193
+ ${Hn(a)}
194
+ <div class="pane${a.view==="tasks"?" act":""}">${Jn(i,t,a)}</div>
195
+ <div class="pane${a.view==="control"?" act":""}">${On(i)}</div>
196
+ <div class="pane${a.view==="billing"?" act":""}">${Vn(i,a)}</div>
197
+ <div class="suggslot">${In(a,i.harness)}</div>
198
+ ${te(s)?"":`<div class="licfoot">${me("licfootlink")}</div>`}
199
+ </div>`}function Yn(e,n){const t=e.narratives?.[n]??null,s=C(t,e.sessionSel?.[n]);return{view:e.view??$e.view,narrative:t!==null&&s!==null?t.states[s]??null:null,sessions:t?.sessions??[],selectedSession:s,sessionSeen:e.sessionSeen??{},suggHidden:e.suggHidden??{},island:e.island??!1,todoOpen:e.todoOpen??{},nodeOpen:e.nodeOpen??{},rawOpen:e.rawOpen??!1}}function Qn(e,n){if(e.error)return`<div class="wrap">${F(e.error)}</div>`;const t=e.configs.find(D=>D.harness===e.active)??e.configs[0];if(!t)return`<div class="wrap">${F()}</div>`;const s=Yn(e,t.harness),a=ae(e.live.entries,t.harness,t.capabilities.live,n),i=Q(t),c=i?`Σ ${k(i)} tok`:"Σ —",u=ie(e.narratives?.[t.harness]??null),l=pn(s.narrative,a,c,u,n),p=e.flow?An(e.flow):"",v=e.license??null,g=e.licenseOpen&&v?jn(v):"",w=e.importResult?xn(e.importResult):"",m=e.share?Mn(e.share):"";return`<div class="wrap${e.open?" open":""}">
200
+ ${yn(t,l)}
201
+ ${Xn(e.configs,t.harness,e.live,v,s)}
202
+ </div>${p}${g}${w}${m}`}const S=Fe(window.location.search);S&&document.documentElement.classList.add("island");const h={entries:[],connection:"connecting",replaying:!1},o={configs:[],active:"",open:!S,error:null,live:h,flow:null,license:null,licenseOpen:!1,importResult:null,share:null,view:"tasks",narratives:{},sessionSel:{},sessionSeen:{},suggHidden:{},island:S,todoOpen:{},nodeOpen:{},rawOpen:!1};let j=null,H=0;const J=document.querySelector("#app");let V="";function d(){if(!J)return;const e=Qn(o,Date.now());e!==V&&(V=e,J.innerHTML=e),S&&B()}function B(){let e=null;if(!o.open){const t=document.querySelector(".pill")??document.querySelector(".empty-card");if(t){const s=t.getBoundingClientRect();e={x:s.x,y:s.y,w:s.width,h:s.height}}}const n=qe(o.open,e,j,Zn());window.location.hash!==n&&history.replaceState(null,"",n)}function Zn(){let e=null;for(const n of I){const t=ie(o.narratives?.[n]??null);if(!t)continue;const s=Date.parse(t.since);Number.isFinite(s)&&(e===null||s>e.sinceMs)&&(e={sinceMs:s,message:`${T[n]} 在等你:${t.message}`})}return e}function zn(e){H+=1,j={target:e,seq:H},B();const n=H;setTimeout(()=>{j?.seq===n&&(j=null,B())},600)}function et(e){if(!Array.isArray(e))throw new Error("载荷不是 JSON 数组");for(const n of e){if(typeof n!="object"||n===null)throw new Error("载荷元素不是对象");const t=n;if(typeof t.harness!="string"||typeof t.scannedAt!="string")throw new Error("载荷元素缺少 harness/scannedAt 字段");const s=t.slots;for(const a of L)if(!s||!Array.isArray(s[a]))throw new Error(`harness ${t.harness} 缺少槽位 ${a}`)}return e}async function ge(){try{const e=await fetch("/api/anatomy");if(!e.ok)throw new Error(`GET /api/anatomy → HTTP ${e.status}`);const n=et(await e.json());o.configs=n,o.active=n.some(t=>t.harness===o.active)?o.active:n[0]?.harness??"",o.error=null}catch(e){o.error=e instanceof Error?e.message:String(e)}d()}async function nt(){try{const e=await fetch("/api/license");if(!e.ok)return;const n=ne(await e.json());n&&(o.license=n,d())}catch{}}function tt(){const e=document.createElement("input");e.type="file",e.accept=".json,application/json";const n=()=>{S&&!o.open&&(o.open=!0,d())};if(e.addEventListener("change",()=>{n();const t=e.files?.[0];t&&be(t)}),e.addEventListener("cancel",n),S&&o.open){o.open=!1,d(),window.setTimeout(()=>e.click(),200);return}e.click()}async function be(e){let n;try{n=await e.text()}catch(t){o.importResult={ok:!1,message:`读取文件失败:${O(t)}`},d();return}await st(n)}async function st(e){let n;try{n=await fetch("/api/license/install",{method:"POST",headers:{"content-type":"application/json"},body:e})}catch(s){o.importResult={ok:!1,message:`导入请求失败:${O(s)}`},d();return}if(!n.ok){o.importResult={ok:!1,message:await A(n)},d();return}let t=null;try{t=ne(await n.json())}catch{t=null}t&&(o.license=t),o.importResult={ok:!0,message:Ve(t)},d()}if(S)document.addEventListener("dragover",e=>e.preventDefault()),document.addEventListener("drop",e=>e.preventDefault());else{const e=document.createElement("div");e.id="drop",e.textContent="松开导入 license",document.body.appendChild(e);let n=0;const t=s=>Array.from(s.dataTransfer?.types??[]).includes("Files");document.addEventListener("dragenter",s=>{t(s)&&(s.preventDefault(),n+=1,e.style.display="flex")}),document.addEventListener("dragover",s=>{t(s)&&s.preventDefault()}),document.addEventListener("dragleave",s=>{t(s)&&(n=Math.max(0,n-1),n===0&&(e.style.display="none"))}),document.addEventListener("drop",s=>{if(!t(s))return;s.preventDefault(),n=0,e.style.display="none";const a=Array.from(s.dataTransfer?.files??[]),i=Ge(a.map(u=>u.name));if(i){o.importResult={ok:!1,message:i},d();return}const c=a[0];c&&be(c)})}const at=300,U={};async function W(e){try{const n=await fetch(`/api/narrative?harness=${e}`);if(!n.ok)return;const t=an(await n.json());if(t){o.narratives||(o.narratives={}),o.narratives[e]=t;const s=C(t,o.sessionSel?.[e]);s!==null&&(o.sessionSel||(o.sessionSel={}),o.sessionSel[e]=s,we(e,s)),d()}}catch{}}function we(e,n){const t=o.narratives?.[e]?.sessions.find(s=>s.id===n);t&&(o.sessionSeen||(o.sessionSeen={}),o.sessionSeen[`${e}:${n}`]=t.lastActivity)}function it(e){U[e]===void 0&&(U[e]=window.setTimeout(()=>{delete U[e],W(e)},at))}function ot(){const e=o.configs.find(s=>s.harness===o.active)??o.configs[0];if(!e)return null;const n=o.narratives?.[e.harness]??null,t=C(n,o.sessionSel?.[e.harness]);return n===null||t===null?null:n.states[t]??null}function rt(e){const n=o.todoOpen??{};let t;if(e==="__other__")t=n[e]!==!1;else{const s=ot()?.todos.find(a=>a.id===e);t=s?ue(s,n):n[e]===!0}o.todoOpen={...n,[e]:!t},d()}const I=["cc","codex","dsh"];function ct(e){if(typeof e!="object"||e===null)return null;const n=e;if(!I.includes(n.harness)||typeof n.received_at!="string")return null;const t=n.payload;return typeof t!="object"||t===null||typeof t.hook_event_name!="string"?null:e}function ye(e,n){h.entries=Ze(h.entries,n?{event:e,mock:n}:{event:e}),it(e.harness),d()}function lt(){const e=new EventSource("/api/events");e.onopen=()=>{h.connection!=="open"&&(h.connection="open",d())},e.onmessage=n=>{try{const t=ct(JSON.parse(n.data));t&&ye(t)}catch{}},e.onerror=()=>{h.connection!=="retrying"&&(h.connection="retrying",d())}}const dt=200;async function ut(){if(!h.replaying){h.replaying=!0,d();try{const e=await fetch("/api/live-demo.json");if(!e.ok)throw new Error(`GET /api/live-demo.json → HTTP ${e.status}`);const n=await e.json();for(const t of n.events)ye({harness:n.harness,received_at:new Date().toISOString(),payload:t},!0),await new Promise(s=>setTimeout(s,dt))}catch{}h.replaying=!1,d()}}let $=0;function P(){return o.configs.find(e=>e.harness===o.active)??o.configs[0]}function pt(e,n){for(const t of L){const s=(e.slots[t]??[]).find(a=>a.id===n);if(s)return s}}function ke(){$+=1,o.flow=null,d()}function O(e){return e instanceof Error?e.message:String(e)}async function A(e){const n=await e.text();try{const t=JSON.parse(n);if(typeof t.error=="string"&&t.error!=="")return t.error}catch{}return`HTTP ${e.status}${n?` — ${n}`:""}`}async function ft(e,n){try{const t=await fetch("/api/action-plan-demo.json");if(!t.ok)throw new Error(`GET /api/action-plan-demo.json → HTTP ${t.status}`);const s=await t.json(),a=typeof s.__mock_note=="string"?s.__mock_note:"";if(!a.includes("MOCK"))throw new Error("演示件缺 MOCK 标注(增补条款 10)");const i=ee(s.plan);if(e!==$)return;o.flow={phase:"confirm",title:z(Me(i),i.action.enable),plan:i,mockNote:a}}catch(t){if(e!==$)return;o.flow={phase:"error",title:n,message:`服务器无动作端点(dev 模式?),且演示件不可用:${O(t)}`}}d()}async function G(e,n){const t=P();if(!t)return;const s=pt(t,e);if(!s)return;const a=n??!s.enabled,i=z(s.nativeName,a),c=++$;o.flow={phase:"planning",title:i},d();const u={kind:"toggle",itemId:e,enable:a};let l=null;try{l=await fetch("/api/action/plan",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({harness:t.harness,action:u})})}catch{l=null}if(c===$){if(l===null||l.status===404||l.status===405){await ft(c,i);return}if(!l.ok){o.flow={phase:"error",title:i,message:await A(l)},d();return}try{const p=ee(await l.json());if(c!==$)return;o.flow={phase:"confirm",title:i,plan:p}}catch(p){if(c!==$)return;o.flow={phase:"error",title:i,message:O(p)}}d()}}async function vt(){if(o.flow?.phase!=="confirm")return;const{title:e,plan:n,mockNote:t}=o.flow;if(t){o.flow={phase:"demo",title:e},d();return}const s=++$;o.flow={phase:"applying",title:e},d();let a;try{a=await fetch("/api/action/apply",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({harness:n.harness,plan:n})})}catch(i){if(s!==$)return;o.flow={phase:"error",title:e,message:`apply 请求失败:${O(i)}`},d();return}if(s===$){if(a.status===403){const i=await A(a);if(s!==$)return;o.flow=Ue(e,i),d();return}if(!a.ok){o.flow={phase:"error",title:e,message:await A(a)},d();return}try{const i=He(await a.json());if(s!==$)return;i.ok?(o.flow={phase:"applied",title:e,backupDir:i.backupDir},ge()):o.flow={phase:"error",title:e,message:i.error}}catch(i){if(s!==$)return;o.flow={phase:"error",title:e,message:O(i)}}d()}}let _=0;function mt(){const e=P();e&&(_+=1,o.share={kind:"config",title:`分享配置样本 · ${T[e.harness]}`,lines:mn(e),phase:"preview"},d())}function $t(){_+=1,o.share={kind:"events",title:"分享事件样本",lines:$n(h.entries),phase:"preview"},d()}function Se(){_+=1,o.share=null,d()}function ht(e){o.share?.phase==="preview"&&(o.share={...o.share,lines:hn(o.share.lines,e)},d())}async function gt(){const e=o.share;if(e?.phase!=="preview"||e.lines.length===0||ve(e.kind,e.lines))return;const n=++_;o.share={...e,phase:"sending"},d();let t;try{t=await fetch(fn,{method:"POST",headers:{"content-type":"application/json"},body:pe(e.kind,e.lines)})}catch(s){if(n!==_)return;o.share={...e,phase:"error",message:`发送失败:${O(s)}`},d();return}if(n===_){if(t.ok)o.share={...e,phase:"sent"};else{const s=await A(t);if(n!==_)return;o.share={...e,phase:"error",message:s}}d()}}document.addEventListener("click",e=>{const n=e.target;if(!n)return;if(Ke({island:S,open:o.open,inPanel:n.closest(".panel")!==null||n.closest(".pill")!==null||n.closest(".overlay")!==null,modalOpen:!!(o.flow||o.share||o.licenseOpen||o.importResult)})){o.open=!1,d();return}const t=n.closest('[data-act="locate"]');if(t instanceof HTMLElement&&t.dataset.target){zn(t.dataset.target);return}const s=n.closest('[data-act="view"]');if(s instanceof HTMLElement&&s.dataset.view){o.view=s.dataset.view,d();return}const a=n.closest('[data-act="session"]');if(a instanceof HTMLElement&&a.dataset.id!==void 0){const m=P();m&&(o.sessionSel||(o.sessionSel={}),o.sessionSel[m.harness]=a.dataset.id,we(m.harness,a.dataset.id),d());return}const i=n.closest('[data-act="sugg-plan"]');if(i instanceof HTMLElement&&i.dataset.item!==void 0){G(i.dataset.item,i.dataset.enable==="1");return}const c=n.closest('[data-act="sugg-dismiss"]');if(c instanceof HTMLElement&&c.dataset.sugg!==void 0){const m=P();if(m){const D=o.narratives?.[m.harness]??null,Te=C(D,o.sessionSel?.[m.harness])??"";o.suggHidden||(o.suggHidden={}),o.suggHidden[`${m.harness}:${Te}:${c.dataset.sugg}`]=!0,d()}return}const u=n.closest('[data-act="todo"]');if(u instanceof HTMLElement&&u.dataset.id!==void 0){rt(u.dataset.id);return}const l=n.closest('[data-act="node"]');if(l instanceof HTMLElement&&l.dataset.key!==void 0){const m=o.nodeOpen??{};o.nodeOpen={...m,[l.dataset.key]:m[l.dataset.key]!==!0},d();return}if(n.closest('[data-act="raw"]')){o.rawOpen=!o.rawOpen,d();return}if(n.closest('[data-act="toggle"]')){o.open=!o.open,d();return}if(n.closest('[data-act="demo"]')){ut();return}if(n.closest('[data-act="lic-open"]')){o.licenseOpen=!0,d();return}if(n.closest('[data-act="lic-close"]')){o.licenseOpen=!1,d();return}if(n.closest('[data-act="lic-import"]')){tt();return}if(n.closest('[data-act="import-close"]')){o.importResult=null,d();return}if(n.closest('[data-act="share-config"]')){mt();return}if(n.closest('[data-act="share-events"]')){$t();return}const v=n.closest('[data-act="share-del"]');if(v instanceof HTMLElement&&v.dataset.i!==void 0){ht(Number(v.dataset.i));return}if(n.closest('[data-act="share-send"]')){gt();return}if(n.closest('[data-act="share-close"]')){Se();return}const g=n.closest('[data-act="tgl"]');if(g instanceof HTMLElement&&g.dataset.id){G(g.dataset.id);return}if(n.closest('[data-act="plan-confirm"]')){vt();return}if(n.closest('[data-act="plan-cancel"]')){ke();return}const w=n.closest("[data-tab]");w instanceof HTMLElement&&w.dataset.tab&&(o.active=w.dataset.tab,I.includes(w.dataset.tab)&&W(w.dataset.tab),d())});document.addEventListener("keydown",e=>{if(e.key==="Escape"){if(o.share){Se();return}if(o.importResult){o.importResult=null,d();return}if(o.licenseOpen){o.licenseOpen=!1,d();return}if(o.flow){o.flow.phase!=="applying"&&ke();return}S&&o.open&&(o.open=!1,d())}});const b=document.createElement("div");b.id="tip";document.body.appendChild(b);document.addEventListener("mousemove",e=>{const t=e.target?.closest("[data-tip]");if(!(t instanceof HTMLElement)||!t.dataset.tip){b.style.display="none";return}b.textContent=t.dataset.tip,b.style.display="block";const s=b.offsetWidth;let a=e.clientX+14;a+s>window.innerWidth-8&&(a=e.clientX-s-14),b.style.left=`${a}px`,b.style.top=`${e.clientY+14}px`});document.addEventListener("mousedown",()=>{b.style.display="none"});window.addEventListener("blur",()=>{b.style.display="none"});let X="";setInterval(()=>{const e=o.configs.find(s=>s.harness===o.active)??o.configs[0];if(!e||h.entries.length===0)return;const n=ae(h.entries,e.harness,e.capabilities.live,Date.now()),t=`${n.active}:${n.label}`;t!==X&&(X=t,d())},1e3);ge();nt();for(const e of I)W(e);lt();
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Wingman</title>
7
- <script type="module" crossorigin src="/assets/index-CHt8loKP.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-N-unp8i7.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CP99dQS1.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-wingman",
3
- "version": "0.3.0",
4
- "description": "Wingman — 本地 harness 配置面板(Claude Code / Codex / dsh)。只绑 127.0.0.1,零遥测。",
3
+ "version": "0.4.0",
4
+ "description": "Wingman — 本地 harness 配置面板(Claude Code / Codex / dsh)。面板只绑 127.0.0.1;匿名使用统计不含内容与路径,口径见 getwingman.dev/data。",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
7
7
  "wingman": "apps/cli/dist/main.js"
@@ -1,202 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const c of i.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&n(c)}).observe(document,{childList:!0,subtree:!0});function s(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(a){if(a.ep)return;a.ep=!0;const i=s(a);fetch(a.href,i)}})();const L=["base","rules","skills","tools","gates","history"],T={base:"基础指令",rules:"你的规则",skills:"技能包",tools:"工具定义",gates:"闸门 hooks",history:"对话历史"},M={base:"#3987e5",rules:"#d95926",skills:"#199e70",tools:"#c98500",gates:"#898781",history:"#d55181"},S={cc:"Claude Code",codex:"Codex",dsh:"dsh"},K={cc:{base:"内置 system prompt",rules:"CLAUDE.md · rules/*.md",skills:"Skills · SKILL.md",tools:"MCP servers",gates:"hooks · permissions",history:"projects/*.jsonl"},codex:{base:"内置 instructions",rules:"AGENTS.md · config.toml",skills:"plugins",tools:"mcp_servers",gates:"approval_policy · hooks",history:"history.jsonl"},dsh:{base:"core 插件(一切皆插件)",rules:"profile · patch",skills:"skill 插件",tools:"tool-* 插件",gates:"approval · permission 插件",history:"session log 插件"}};function w(e){return e<1e3?String(e):`${(Math.round(e/100)/10).toFixed(1)}k`}function y(e){if(!e)return"—";const t=w(e.value);return e.kind==="estimate"?`约 ${t}`:t}function Te(e){return e.onTriggerTokens?`常驻 ${y(e.tokens)} · 触发后 ${y(e.onTriggerTokens)}`:y(e.tokens)}function X(e){let t=0,s=!1,n="exact";for(const a of e)!a.enabled||!a.tokens||(t+=a.tokens.value,s=!0,a.tokens.kind==="estimate"&&(n="estimate"));return s?{kind:n,value:t}:void 0}function Y(e){let t=0,s=!1,n="exact";for(const a of L){const i=X(e.slots[a]??[]);i&&(t+=i.value,s=!0,i.kind==="estimate"&&(n="estimate"))}return s?{kind:n,value:t}:void 0}function Ee(e){return e==="full"?"完整 · 插件 API":e==="push"?"推送 · hooks":"未接线"}function _e(e){return`${e.slice(0,16).replace("T"," ")} UTC`}function Q(e){const t=[];return t.push(e.nativePath?`文件:${e.nativePath}`:`无文件实体(source: ${e.source})`),e.__source_note&&t.push(e.__source_note),(e.tokens?.kind==="estimate"||e.onTriggerTokens?.kind==="estimate")&&t.push("估算 = 字节 ÷ 3.5,故标「约」"),e.tokens?.kind==="exact"&&t.push(`精确值(harness 自报 usage):${e.tokens.value.toLocaleString("en-US")} tokens`),t.join(`
2
- `)}function Oe(e){const t=[],s=[];for(const n of L){const a=(e.slots[n]??[]).filter(i=>i.toggleable);a.length>0?t.push({slot:n,items:a}):s.push(n)}return{groups:t,slotsWithoutToggle:s}}function Le(e){return e.tokens?e.enabled?`关掉可省 ${y(e.tokens)}`:`开启将 +${y(e.tokens)}`:""}function N(e){return e===""?0:e.split(`
3
- `).length-(e.endsWith(`
4
- `)?1:0)}function Ae(e){return e==="create"?"新建":e==="delete"?"删除":"修改"}function Ne(e){return e.kind==="create"?`${N(e.after??"")} 行`:e.kind==="delete"?`原 ${N(e.before??"")} 行`:`${N(e.before??"")} 行 → ${N(e.after??"")} 行`}function je(e){return e==="immediate"?"立即生效 —— 该机制被 harness 实时监听,当前 session 也会更新":e==="reconnect"?"重连后生效 —— 需重启该 harness(或重连其 MCP server)才加载":"下个 session 生效 —— 正在进行的对话不受影响,新开会话才加载新配置"}function Z(e,t){return`${t?"打开":"关闭"} ${e}`}function xe(e){const t=e.action.itemId.split(":");return t.length>=4?t.slice(3).join(":"):e.action.itemId}const Me=["cc","codex","dsh"],Pe=["immediate","next_session","reconnect"],Ce=["create","modify","delete"];function f(e){throw new Error(`plan 载荷不合合同,拒绝渲染:${e}`)}function Re(e,t){(typeof e!="object"||e===null)&&f(`writes[${t}] 不是对象`);const s=e;(typeof s.file!="string"||s.file==="")&&f(`writes[${t}].file 不是非空字符串`),Ce.includes(s.kind)||f(`writes[${t}].kind 未知值 ${JSON.stringify(s.kind)}`),s.kind!=="create"&&typeof s.before!="string"&&f(`writes[${t}].before 缺失(kind=${String(s.kind)} 必须携带变更前完整内容)`),s.kind!=="delete"&&typeof s.after!="string"&&f(`writes[${t}].after 缺失(kind=${String(s.kind)} 必须携带变更后完整内容)`)}function Ie(e,t){(typeof e!="object"||e===null)&&f(`references[${t}] 不是对象`);const s=e;(typeof s.file!="string"||s.file==="")&&f(`references[${t}].file 不是非空字符串`),(typeof s.line!="number"||!Number.isInteger(s.line)||s.line<1)&&f(`references[${t}].line 不是 1-based 行号`),typeof s.matchedName!="string"&&f(`references[${t}].matchedName 不是字符串`),typeof s.context!="string"&&f(`references[${t}].context 不是字符串`)}function z(e){(typeof e!="object"||e===null)&&f("不是对象");const t=e;Me.includes(t.harness)||f(`harness 未知值 ${JSON.stringify(t.harness)}`),(typeof t.action!="object"||t.action===null)&&f("action 不是对象");const s=t.action;return s.kind!=="toggle"&&f(`action.kind 未知值 ${JSON.stringify(s.kind)}(v1 仅 toggle 走 plan)`),(typeof s.itemId!="string"||s.itemId==="")&&f("action.itemId 不是非空字符串"),typeof s.enable!="boolean"&&f("action.enable 不是布尔值"),Array.isArray(t.writes)||f("writes 不是数组"),t.writes.forEach(Re),(typeof t.backupDir!="string"||t.backupDir==="")&&f("backupDir 不是非空字符串"),Array.isArray(t.references)||f("references 不是数组"),t.references.forEach(Ie),Pe.includes(t.effectiveAt)||f(`effectiveAt 未知值 ${JSON.stringify(t.effectiveAt)}`),e}function De(e){if(typeof e!="object"||e===null)throw new Error("apply 响应不合合同:不是对象");const t=e;if(t.ok===!0){if(typeof t.backupDir!="string"||t.backupDir==="")throw new Error("apply 响应不合合同:ok=true 但缺 backupDir");return{ok:!0,backupDir:t.backupDir}}if(t.ok===!1){if(typeof t.error!="string"||t.error==="")throw new Error("apply 响应不合合同:ok=false 但缺 error 原文");return{ok:!1,error:t.error}}throw new Error("apply 响应不合合同:ok 不是布尔值")}function He(e,t){return t.includes("fixtures 模式只读")?{phase:"demo",title:e}:{phase:"error",title:e,message:t}}function Ue(e){return new URLSearchParams(e).get("island")==="1"}function Fe(e,t,s=null,n=null){const a=e?"#island=open":`#island=pill:${qe(t??{x:0,y:0,w:0,h:0})}`,i=s?`;focus:${s.target},${s.seq}`:"",c=n?`;waiting:${Math.round(n.sinceMs)},${encodeURIComponent(n.message)}`:"";return`${a}${i}${c}`}function qe(e){return`${Math.round(e.x)},${Math.round(e.y)},${Math.round(e.w)},${Math.round(e.h)}`}function Be(e){return e.island&&e.open&&!e.inPanel&&!e.modalOpen}const Ke="https://billing.stripe.com/p/login/14AcMZ8Z147F4MmaTKcMM00",We=["none","invalid","active","grace","expired"];function ee(e){if(typeof e!="object"||e===null)return null;const t=e;return!We.includes(t.state)||typeof t.detail!="string"?null:{state:t.state,detail:t.detail}}function te(e){return e&&(e.state==="grace"||e.state==="expired")?e.state:null}function Je(e){return e?`license 已导入 —— ${e.detail}`:"license 已导入(新状态未知,刷新本页查看)"}function Ve(e){if(e.length!==1)return`一次拖入 1 个 license.json 文件(收到 ${e.length} 个)`;const t=e[0]??"";return t.toLowerCase().endsWith(".json")?null:`只认 .json 文件(收到 ${t})`}const Ge=50,Xe=15e3,Ye={SessionStart:"会话开始",SessionEnd:"会话结束",UserPromptSubmit:"提交提示",PreToolUse:"工具调用开始",PostToolUse:"工具调用完成",PostToolBatch:"工具批次完成",Stop:"回合结束",SubagentStart:"子代理启动",SubagentStop:"子代理结束",PreCompact:"压缩开始",PostCompact:"压缩完成",ConfigChange:"配置变更",InstructionsLoaded:"指令加载",Notification:"通知",PermissionRequest:"权限请求"};function se(e){return Ye[e]??e}function Qe(e,t,s=Ge){const n=[...e,t];return n.length>s?n.slice(n.length-s):n}function Ze(e){return e.slice(11,19)}function ze(e){const t={cc:0,codex:0,dsh:0};for(const{event:s}of e)t[s.harness]+=1;return t}function et(e,t){const s=[];for(const{event:n}of e){if(n.harness!==t)continue;const{hook_event_name:a,session_id:i,tool_name:c}=n.payload,u=typeof i=="string"?i:"";if(a==="PreToolUse"&&typeof c=="string")s.push({session:u,tool:c});else if(a==="PostToolUse"&&typeof c=="string")for(let l=s.length-1;l>=0;l--){const p=s[l];if(p&&p.session===u&&p.tool===c){s.splice(l,1);break}}else if(a==="Stop"||a==="SessionEnd")for(let l=s.length-1;l>=0;l--)s[l]?.session===u&&s.splice(l,1)}return s[s.length-1]?.tool}function ne(e,t,s,n){const a=e.filter(l=>l.event.harness===t),i=a[a.length-1];if(!i)return{active:!1,label:s==="none"?"未接线":"等待事件"};const c=Date.parse(i.event.received_at);if(!Number.isFinite(c)||n-c>Xe)return{active:!1,label:"静默"};const u=et(e,t);return{active:!0,label:u?`${u} 运行中`:se(i.event.payload.hook_event_name)}}function tt(e){return e.replaying?"演示重放中 · MOCK":e.connection==="open"?"实况已连接":e.connection==="retrying"?"连接断开 · 自动重连中":"连接中…"}function st(e){if(typeof e!="object"||e===null)return null;const t=e;return typeof t.harness!="string"||!Array.isArray(t.todos)||!Array.isArray(t.traces)||!Array.isArray(t.turnUsages)||typeof t.updatedAt!="string"?null:e}function nt(e){if(typeof e!="object"||e===null)return null;const t=e;if(typeof t.harness!="string"||!Array.isArray(t.sessions))return null;for(const s of t.sessions){if(typeof s!="object"||s===null)return null;const n=s;if(typeof n.id!="string"||typeof n.label!="string"||typeof n.lastActivity!="string"||typeof n.waiting!="boolean"||typeof n.ended!="boolean")return null}if(typeof t.states!="object"||t.states===null||Array.isArray(t.states))return null;for(const s of Object.values(t.states))if(st(s)===null)return null;return e}function C(e,t){return!e||e.sessions.length===0?null:t!==void 0&&e.states[t]!==void 0?t:e.sessions[0]?.id??null}function ae(e){if(!e)return null;for(const t of e.sessions){if(!t.waiting)continue;const s=e.states[t.id]?.waiting;if(s)return{label:t.label,message:s.message,since:s.since}}return null}function ie(e){return e?e.todos.length>0||e.traces.length>0||e.turnUsages.length>0||e.currentAction!==null||e.waiting!==null:!1}const at={completed:"✓",in_progress:"▶",pending:"○"};function it(e){const t=e.todos.find(s=>s.status==="in_progress");return t?t.activeForm??t.content:e.lastPrompt}function oe(e){return e?e.kind==="tool"?e.detail===""?e.toolName:`${e.toolName} · ${e.detail}`:re(e.name):null}const ot={SessionStart:"会话开始",SessionEnd:"会话结束",UserPromptSubmit:"收到新提示",PreToolUse:"工具调用开始",PostToolUse:"工具调用完成",Stop:"回合结束",SubagentStart:"子代理启动",SubagentStop:"子代理结束",PreCompact:"压缩开始",PostCompact:"压缩完成"};function re(e){return ot[e]??e}function ce(e){return e?e.turnUsages[e.turnUsages.length-1]??null:null}function le(e){return e<.005?"<$0.01":`$${e.toFixed(2)}`}function j(e){const t=`${w(e.contextTotal.value)} tok`;return e.costUsd===void 0?t:`${t} ≈ ${le(e.costUsd)}`}function R(e){if(e<1e3)return"<1s";const t=Math.round(e/1e3);return t<60?`${t}s`:`${Math.floor(t/60)}m${t%60===0?"":` ${t%60}s`}`}function rt(e,t){return e.agents.length>0?"子 agent":t&&t.nodes.length>0?"主 agent":null}function ct(e){return!e||e.todos.length===0?null:`${e.todos.filter(s=>s.status==="completed").length}/${e.todos.length}`}const lt=6e4;function dt(e,t,s,n,a){const i=ce(e),c=n!==void 0?n:e?.waiting?{label:"",message:e.waiting.message,since:e.waiting.since}:null;if(c){const u=c.label===""?"":`(${c.label})`,l=Date.parse(c.since),p=a!==void 0&&Number.isFinite(l)?a-l:Number.NaN,v=Number.isFinite(p)&&p>lt?` · 已等 ${R(p)}`:"";return{mode:"waiting",label:`等你${u}:${c.message}${v}`,tok:i?j(i):s}}return t.active?{mode:"working",label:(e?oe(e.currentAction):null)??t.label,tok:i?`本轮 ${j(i)}`:s}:{mode:"idle",label:t.label,tok:i?`上轮 ${j(i)}`:s}}function de(e,t){const s=t[e.id];return s!==void 0?s:e.status==="in_progress"}const ut="https://api.getwingman.dev/samples",pt=256*1024;function ft(e){return e.__mock_note?[]:JSON.stringify(e,null,2).split(`
5
- `)}function vt(e){return e.filter(t=>!t.mock).map(t=>JSON.stringify(t.event))}function mt(e,t){return!Number.isInteger(t)||t<0||t>=e.length?[...e]:e.filter((s,n)=>n!==t)}function $t(e){return e.join(`
6
- `)}function ue(e,t){return JSON.stringify({kind:e,sample:$t(t)})}function pe(e,t){return new TextEncoder().encode(ue(e,t)).length}function fe(e,t){return pe(e,t)>pt}function gt(e,t){const s=(pe(e,t)/1024).toFixed(1);return`共 ${t.length} 行 · 约 ${s} KB / 上限 256 KB`}function ht(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}const o=ht;function F(e){return`<div class="empty-card">
7
- <div class="empty-title">未检测到 harness</div>
8
- ${e?`<div class="empty-detail">${o(e)}</div>`:'<div class="empty-detail">/api/anatomy 返回了空数组</div>'}
9
- <div class="empty-hint">先运行 <code>wingman serve</code>,面板从 GET /api/anatomy 读取解剖数据。</div>
10
- </div>`}function bt(e,t){const s=t.mode==="waiting"?" wait":t.mode==="working"?" live":" quiet",n=t.mode==="waiting"?"有未决的权限请求/通知(PermissionRequest/Notification 事件);点击展开处理":t.mode==="working"?"有活跃 session(最近 15s 内收到 hook 事件);点击展开面板":"点击展开面板";return`<div class="pill${t.mode==="waiting"?" waiting":""}" data-act="toggle" data-tip="${o(n)}">
11
- <span class="dot${s}"></span>
12
- <span class="t1">${o(S[e.harness])}</span>
13
- <span class="t2">${o(t.label)}</span>
14
- ${t.tok===""?"":`<span class="tok">${o(t.tok)}</span>`}
15
- <span class="chev">▾</span>
16
- </div>`}function wt(e,t,s){return`<div class="tabs">${e.map(a=>{const i=a.harness===t,c=s[a.harness],u=!i&&c>0?`<span class="tabnum" data-tip="实况流内 ${S[a.harness]} 事件 ${c} 条">${c}</span>`:"";return`<button class="tab${i?" act":""}" data-tab="${a.harness}">${o(S[a.harness])}${u}</button>`}).join("")}</div>`}function yt(e){const t=L.map(l=>({slot:l,total:X(e.slots[l]??[])})),s=t.reduce((l,p)=>l+(p.total?.value??0),0),n=Y(e),a=n?`常驻合计 <b data-tip="合计 = 各槽位启用条目的常驻 token 之和;${n.kind==="estimate"?"含估算值,故标「约」":"全部为 harness 自报精确值"}">${o(y(n))} token</b>`:"本次扫描未采到 token 数值(无可估算的字节来源)",i=s>0?t.filter(l=>l.total&&l.total.value>0).map(l=>{const p=(l.total?.value??0)/s*100,v=`${T[l.slot]} · ${y(l.total)}`;return`<div class="seg" style="width:${p.toFixed(1)}%;background:${M[l.slot]}" data-tip="${o(v)}"></div>`}).join(""):"",c=i?`<div class="stack">${i}</div>`:"",u=t.map(({slot:l,total:p})=>{const v=!p,h=e.slots[l]??[],b=v?`${T[l]}:本次扫描该槽位无带 token 来源的启用条目`:`${T[l]} 启用条目常驻合计(逐条来源见下方明细)`;return`<div class="lrow${v?" na":""}" data-tip="${o(b)}">
17
- <span class="sw" style="background:${v?"var(--grid)":M[l]}"></span>${T[l]}
18
- <span class="nat">${o(K[e.harness][l])}</span>
19
- <span class="v">${o(y(p))}</span>
20
- <span class="cnt">${h.length} 项</span>
21
- </div>`}).join("");return`<div class="sec">
22
- <div class="label">CONTEXT 组成<span class="labr"><button class="sharelink" data-act="share-config" data-tip="打开预览弹层:先看到将发送的完整内容,可逐行删,点「发送」才会发出;关闭 = 什么都不发">分享配置样本</button></span></div>
23
- <div class="headline">${a}</div>
24
- ${c}
25
- <div class="legend">${u}</div>
26
- </div>`}function kt(e){const t=e.enabled?"":'<span class="statechip off" data-tip="该条目当前被关闭;可开关条目在下方 CONFIGURE 区一键打开">已关闭</span>',s=e.nativePath?"":'<span class="statechip nofile" data-tip="无文件实体:该条目不来自可读文件(如内置注入 / boot 树行)">无文件实体</span>';return`<div class="crow${e.enabled?"":" offrow"}" data-tip="${o(Q(e))}">
27
- <span class="cname">${o(e.nativeName)}</span>
28
- <span class="chip">${o(e.source)}</span>
29
- ${t}${s}
30
- <span class="ctok">${o(Te(e))}</span>
31
- </div>`}function St(e){return`<div class="sec">
32
- <div class="label">六槽位明细 · hover 查看来源</div>
33
- ${L.map(s=>{const n=e.slots[s]??[],a=n.length?n.map(i=>kt(i)).join(""):'<div class="crow emptyrow">本次扫描无条目(该槽位在此 harness 无可读来源)</div>';return`<div class="grp">
34
- <div class="gtitle"><span class="gdot" style="background:${M[s]}"></span>${T[s]}
35
- <span class="nat">${o(K[e.harness][s])}</span>
36
- </div>
37
- ${a}
38
- </div>`}).join("")}
39
- </div>`}function Tt(e){const t=Le(e),s=e.enabled?"":'<span class="statechip off">关闭中</span>';return`<div class="crow${e.enabled?"":" offrow"}" data-tip="${o(Q(e))}">
40
- <span class="cname">${o(e.nativeName)}</span>
41
- <span class="chip">${o(e.source)}</span>
42
- ${s}
43
- <span class="ctok">${o(t)}</span>
44
- <span class="cright"><button class="toggle${e.enabled?" on":""}" data-act="tgl" data-id="${o(e.id)}" role="switch" aria-checked="${e.enabled}" aria-label="${o(`${e.enabled?"关闭":"打开"} ${e.nativeName}`)}" data-tip="点击预览改动 —— 先 plan 后 apply:确认前不写任何文件;写前自动备份,可 byte-identical 回退"></button></span>
45
- </div>`}function Et(e){const t=Oe(e);let s;if(t.groups.length===0)s='<div class="crow emptyrow">该 harness 本次扫描没有可在面板开关的条目 —— 开关范围仅限各家原生支持的机制,其余条目请在源文件里改</div>';else if(s=t.groups.map(n=>{const a=n.items.map(c=>Tt(c)).join(""),i=e.harness==="codex"&&n.slot==="tools"?'<div class="cfgnote" data-tip="PLAN §4b 实测:config.toml 的 enabled_tools/disabled_tools 按单个工具开关;CC 只能整 server 开关,dsh 只能整插件开关">Codex 独有:enabled_tools / disabled_tools 可精确到单个工具 —— 三家里粒度最细</div>':"";return`<div class="grp">
46
- <div class="gtitle"><span class="gdot" style="background:${M[n.slot]}"></span>${T[n.slot]}
47
- <span class="nat">${o(K[e.harness][n.slot])}</span>
48
- </div>
49
- ${a}${i}
50
- </div>`}).join(""),t.slotsWithoutToggle.length>0){const n=t.slotsWithoutToggle.map(a=>T[a]).join(" · ");s+=`<div class="cfgnote">其余槽位(${n})本次扫描无可开关条目 —— 开关范围仅限各家原生支持的机制</div>`}return`<div class="sec">
51
- <div class="label">CONFIGURE</div>
52
- ${s}
53
- </div>`}function _t(e,t){const s=e.writes.length?e.writes.map(i=>`<div class="mfile"><span class="mkind ${i.kind}">${Ae(i.kind)}</span><code class="mpath">${o(i.file)}</code><span class="mlines">${o(Ne(i))}</span></div>`).join(""):'<div class="mtext muted">plan 未包含任何文件变更</div>',n=e.references.length?e.references.map(i=>`<div class="mref"><code class="mpath">${o(`${i.file}:${i.line}`)}</code><span class="mctx">${o(i.context)}</span></div>`).join(""):'<div class="mtext muted">引用语料里没有别处提到它</div>';return`${t?`<div class="mnote">${o(t)}</div>`:""}
54
- <div class="msec"><div class="mlabel">将写入的文件 — 确认前一个字节都不动</div>${s}</div>
55
- <div class="msec"><div class="mlabel">引用命中 ${e.references.length} 处 — 这些地方提到了它;只提醒,不修复</div>${n}</div>
56
- <div class="msec"><div class="mlabel">生效时机</div><div class="mtext">${o(je(e.effectiveAt))}</div></div>
57
- <div class="msec"><div class="mlabel">备份</div><div class="mtext">写入前,原件先备份到 <code>${o(e.backupDir)}</code>;回退 = 原样拷回(byte-identical)</div></div>
58
- <div class="mact">
59
- <button class="btn ghost" data-act="plan-cancel">取消</button>
60
- <button class="btn primary" data-act="plan-confirm">确认写入</button>
61
- </div>`}function Ot(e){const t=e.phase==="confirm"&&e.mockNote?`<span class="mockchip" data-tip="${o(e.mockNote)}">MOCK</span>`:"";let s;e.phase==="planning"?s='<div class="mtext muted">正在生成 plan…(纯读:不写盘、不建备份目录)</div>':e.phase==="applying"?s='<div class="mtext muted">正在写入…(原件已先备份到 plan 里的备份目录)</div>':e.phase==="confirm"?s=_t(e.plan,e.mockNote):e.phase==="applied"?s=`<div class="mtext">已写入,面板数据已重拉刷新。</div>
62
- <div class="mtext">备份:<code>${o(e.backupDir)}</code>(回退 = 原样拷回)</div>
63
- <div class="mact"><button class="btn primary" data-act="plan-cancel">关闭</button></div>`:e.phase==="demo"?s=`<div class="mtext">演示模式:动作只到 plan 为止 —— 当前数据源只读(fixtures / dev 演示),不写任何真实文件。</div>
64
- <div class="mact"><button class="btn primary" data-act="plan-cancel">知道了</button></div>`:s=`<div class="merror">${o(e.message)}</div>
65
- <div class="mtext muted">常见原因:文件在 plan 之后被外部改过(关闭后重新点开关生成新 plan)、itemId 不存在、或目标格式未知被拒写(不猜,如实拒绝)。</div>
66
- <div class="mact"><button class="btn ghost" data-act="plan-cancel">关闭</button></div>`;const n=e.phase!=="applying";return`<div class="overlay">
67
- <div class="scrim"${n?' data-act="plan-cancel"':""}></div>
68
- <div class="modal" role="dialog" aria-modal="true" aria-label="${o(e.title)}">
69
- <div class="mhead"><span class="mtitle">${o(e.title)}</span>${t}${n?'<button class="mclose" data-act="plan-cancel" aria-label="关闭弹窗">✕</button>':""}</div>
70
- ${s}
71
- </div>
72
- </div>`}function Lt(e){const t=te(e??null);if(!t||!e)return"";const s=t==="grace"?"订阅已到期,宽限期内 · 点此续期":"订阅已到期,写动作已锁";return`<div class="licrow"><button class="licbar ${t}" data-act="lic-open" data-tip="${o(e.detail)}">${o(s)}</button>${ve("licimport")}</div>`}function ve(e){return`<button class="${e}" data-act="lic-import" data-tip="选择激活页下载的 license.json 导入(写入 ~/.wingman/license.json,旧文件自动备份);也可把文件直接拖进本面板">导入 license</button>`}function At(e){return`<div class="overlay">
73
- <div class="scrim" data-act="import-close"></div>
74
- <div class="modal" role="dialog" aria-modal="true" aria-label="导入 license">
75
- <div class="mhead"><span class="mtitle">导入 license</span><button class="mclose" data-act="import-close" aria-label="关闭弹窗">✕</button></div>
76
- ${e.ok?`<div class="mtext">${o(e.message)}</div>`:`<div class="merror">${o(e.message)}</div>`}
77
- <div class="mact"><button class="btn primary" data-act="import-close">关闭</button></div>
78
- </div>
79
- </div>`}function Nt(e,t=Ke){const s=t===""?"取消订阅或更换付款方式在 Stripe 侧完成,入口在订阅邮箱收到的 Stripe 邮件里。":`<a href="${o(t)}" target="_blank" rel="noreferrer">管理订阅(取消/换卡/发票)</a>`;return`<div class="overlay">
80
- <div class="scrim" data-act="lic-close"></div>
81
- <div class="modal" role="dialog" aria-modal="true" aria-label="续期">
82
- <div class="mhead"><span class="mtitle">续期</span><button class="mclose" data-act="lic-close" aria-label="关闭弹窗">✕</button></div>
83
- <div class="msec"><div class="mlabel">当前状态</div><div class="mtext">${o(e.detail)}</div></div>
84
- <div class="msec"><div class="mlabel">续期</div><div class="mtext">联网运行 <code>wingman license refresh</code>:订阅仍有效即换发新 license(写回 <code>~/.wingman/license.json</code>),刷新本页生效;停订会被拒。</div></div>
85
- <div class="msec"><div class="mlabel">订阅管理</div><div class="mtext">${s}</div></div>
86
- <div class="mact"><button class="btn primary" data-act="lic-close">关闭</button></div>
87
- </div>
88
- </div>`}function jt(e){let t;if(e.phase==="preview"){const s=e.lines.length?e.lines.map((c,u)=>`<div class="shline"><button class="shdel" data-act="share-del" data-i="${u}" aria-label="删除第 ${u+1} 行" data-tip="删掉这一行(只影响本次发送)">✕</button><span class="shtext">${o(c)}</span></div>`).join(""):'<div class="mtext muted">没有可发送的内容(MOCK 演示数据不外发;删光了也一样不发)</div>',n=fe(e.kind,e.lines),a=n?'<div class="merror">样本超过 256 KB 上限,删减后才能发送</div>':"",i=e.lines.length===0||n?" disabled":"";t=`<div class="mtext">分享样本帮助 Wingman 适配更多真实环境。下面就是将要发送的全部内容:逐行检查,不想发的行点 ✕ 删掉;点「发送」才会发出,关闭则什么都不发。样本不含任何账号或设备标识,服务端也不记录 IP。</div>
89
- <div class="msec"><div class="mlabel">将发送的内容 · ${o(gt(e.kind,e.lines))}</div>
90
- <div class="sharebox">${s}</div>${a}</div>
91
- <div class="mact">
92
- <button class="btn ghost" data-act="share-close">关闭(不发送)</button>
93
- <button class="btn primary" data-act="share-send"${i}>发送</button>
94
- </div>`}else e.phase==="sending"?t=`<div class="mtext muted">发送中…(POST ${o("api.getwingman.dev/samples")})</div>`:e.phase==="sent"?t=`<div class="mtext">已发送。样本只用于适配真实环境,90 天后在服务端自动过期。</div>
95
- <div class="mact"><button class="btn primary" data-act="share-close">关闭</button></div>`:t=`<div class="merror">${o(e.message??"发送失败")}</div>
96
- <div class="mtext muted">没有发出任何内容;稍后可重新分享。</div>
97
- <div class="mact"><button class="btn ghost" data-act="share-close">关闭</button></div>`;return`<div class="overlay">
98
- <div class="scrim" data-act="share-close"></div>
99
- <div class="modal" role="dialog" aria-modal="true" aria-label="${o(e.title)}">
100
- <div class="mhead"><span class="mtitle">${o(e.title)}</span><button class="mclose" data-act="share-close" aria-label="关闭弹窗">✕</button></div>
101
- ${t}
102
- </div>
103
- </div>`}function xt(e,t){const{event:s}=e,n=s.payload.hook_event_name,a=typeof s.payload.tool_name=="string"?s.payload.tool_name:"",i=typeof s.payload.session_id=="string"?s.payload.session_id:"(无 session_id)",c=[`事件:${n}(hook_event_name 原文)`,`时间 = receiver 收到时刻 received_at:${s.received_at}`,`session:${i}`];return e.mock&&c.push("MOCK:dev 演示重放注入,非真实 harness 推送(增补条款 10)"),`<div class="lvrow${s.harness===t?" own":" other"}" data-tip="${o(c.join(`
104
- `))}">
105
- <span class="lvtime">${o(Ze(s.received_at))}</span>
106
- <span class="lvname">${o(se(n))}</span>
107
- ${a?`<span class="chip">${o(a)}</span>`:""}
108
- ${e.mock?'<span class="mockchip">MOCK</span>':""}
109
- <span class="lvharness">${o(s.harness)}</span>
110
- </div>`}function Mt(e,t){const s=e.capabilities.live,n=t.entries.filter(c=>c.event.harness===e.harness);let a;t.entries.length>0?(a=`<div class="feed">${[...t.entries].reverse().map(u=>xt(u,e.harness)).join("")}</div>`,s==="none"&&n.length===0&&(a=`<div class="livenone">○ ${o(S[e.harness])} 未接线 —— 运行 <code>wingman hook install ${o(e.harness)}</code> 接线后,其 hook 事件才会出现在下方流里(当前流内是别家事件)</div>`+a)):s==="none"?a=`<div class="livenone">○ 未接线 —— 运行 <code>wingman hook install ${o(e.harness)}</code> 把该 harness 的 hooks 指到本面板(幂等、可 uninstall 恢复);接线前这里不显示任何数据</div>`:a=`<div class="livenone">○ 已接线(${s==="full"?"完整":"推送"}),等待事件 —— 在 ${o(S[e.harness])} 里跑一轮对话即可看到实况流</div>`;const i=t.connection==="retrying"?`<button class="demobtn" data-act="demo" ${t.replaying?"disabled":""} data-tip="EventSource 连不上(dev 模式无 SSE 端点)时的本地演示:以 200ms 间隔重放 fixtures 的 cc 事件序列,注入事件全部带 MOCK 徽章">重放 fixtures 演示 · MOCK</button>`:"";return`<div class="sec">
111
- <div class="label">实况 <span class="tier${s==="none"?" none":""}">${o(Ee(s))}</span>
112
- <span class="conn ${t.replaying?"replay":t.connection}" data-tip="GET /api/events(SSE)连接状态;断线由 EventSource 自动重连">${o(tt(t))}</span>
113
- <span class="labr">${i}<button class="sharelink" data-act="share-events" data-tip="打开预览弹层:先看到将发送的完整事件流,可逐行删,点「发送」才会发出;关闭 = 什么都不发">分享事件样本</button></span>
114
- </div>
115
- ${a}
116
- </div>`}const me={view:"tasks",narrative:null,sessions:[],selectedSession:null,sessionSeen:{},suggHidden:{},island:!1,todoOpen:{},nodeOpen:{},rawOpen:!1};function Pt(e,t){return e.sessions.length<2?"":`<div class="schips">${e.sessions.map(n=>{const a=n.id===e.selectedSession,i=e.sessionSeen[`${t}:${n.id}`]??"",c=!a&&n.lastActivity>i,u=[`session:${n.id}`,`最近活动:${n.lastActivity||"(未观察到)"}`,n.ended?"已结束(SessionEnd,或 Stop 后 10 分钟无事件)":"进行中",n.waiting?"等你:有未决的权限请求/通知":"",c?"有新活动(上次查看后)":""].filter(p=>p!=="").join(`
117
- `),l=n.waiting?'<span class="sdot wait"></span>':c?'<span class="sdot"></span>':"";return`<button class="schip${a?" act":""}${n.ended?" ended":""}" data-act="session" data-id="${o(n.id)}" data-tip="${o(u)}">${l}${o(n.label)}</button>`}).join("")}</div>`}function Ct(e,t){const s=e.narrative?.suggestions??[];if(s.length===0)return"";const n=e.selectedSession??"";return s.filter(a=>e.suggHidden[`${t}:${n}:${a.id}`]!==!0).map(a=>{const i=a.action?`<button class="sugbtn" data-act="sugg-plan" data-item="${o(a.action.itemId)}" data-enable="${a.action.enable?"1":"0"}" data-tip="打开 M3 plan 弹窗:先看方案(写哪些文件/生效时机/备份),确认才写入">看方案</button>`:"";return`<div class="sugg r-${a.rule}" data-tip="${o(a.source)}">
118
- <span class="sugtext">${o(a.text)}</span>${i}<button class="sugdismiss" data-act="sugg-dismiss" data-sugg="${o(a.id)}" data-tip="本 session 不再提示这条(内存态,重开面板恢复)">不再提示</button>
119
- </div>`}).join("")}function Rt(e){const t=it(e),n=e.todos.some(h=>h.status==="in_progress")?"第一条进行中 todo(来源:transcript 的 TaskCreate/TaskUpdate 事件)":"最新用户提示摘要(本会话暂无进行中 todo)",a=oe(e.currentAction),i=ce(e),c=e.__mock_note?`<span class="mockchip" data-tip="${o(e.__mock_note)}">MOCK</span>`:"",u=e.waiting?`<div class="nwait" data-tip="${o(`未决 ${e.waiting.kind==="permission"?"PermissionRequest":"Notification"} 事件,发生于 ${e.waiting.since}`)}">等你:${o(e.waiting.message)}</div>`:"",l=i?`<span data-tip="本轮 context = usage 三字段之和(harness 自报,精确)${i.costUsd===void 0?"":";$ 按内置价目表换算(约)"}">本轮 ${o(j(i))}</span>`:"",p=a!==null?`<div class="ndoing">正在:<b>${o(a)}</b>${l===""?"":` · ${l}`}</div>`:l===""?"":`<div class="ndoing">消耗:${l}</div>`,v=e.agentQuote?`<div class="nquote" data-tip="agent 自述原话(transcript assistant 文本块);引用,不生成">「${o(e.agentQuote)}」</div>`:"";return`<div class="now">
120
- ${u}
121
- <div class="ntask" data-tip="${o(n)}">${t===null?'<span class="muted">(本会话暂无任务信息)</span>':o(t)}${c}</div>
122
- ${p}
123
- ${v}
124
- </div>`}function It(e){const t=ct(e.narrative),s=e.narrative?.turnUsages.length??0,n=(a,i,c)=>`<button class="vtab${e.view===a?" act":""}" data-act="view" data-view="${a}">${i}${c===null?"":` <span class="vcnt">${o(c)}</span>`}</button>`;return`<div class="vtabs">
125
- ${n("tasks","任务",t)}
126
- ${n("control","控制",null)}
127
- ${n("billing","账单",s>0?`${s} 轮`:null)}
128
- </div>`}function Dt(e){const t=e.ts!==void 0&&e.endTs!==void 0?R(Date.parse(e.endTs)-Date.parse(e.ts)):e.endTs===void 0?"…":"",s=e.estTokens===void 0?"":` · 约 ${w(e.estTokens)}`,n=e.isError&&e.errorBrief!==void 0?`<span class="failchip">${o(e.errorBrief)}</span>`:"",a=e.agentId!==void 0?'<span class="ag">子 agent</span>':"";return`<div class="trs${e.isError?" bad":""}" data-tip="${o(`工具:${e.toolName}${e.ts===void 0?"":`
129
- 开始:${e.ts}`}${e.estTokens===void 0?"":`
130
- 约 token = 工具输入+结果字节 ÷ 3.5(估算)`}`)}">
131
- <span class="cmd">${o(e.toolName)}</span>
132
- ${e.detail===""?"":`<span class="sdetail">${o(e.detail)}</span>`}
133
- ${n}${a}
134
- <span class="dur">${o(t)}${o(s)}</span>
135
- </div>`}function Ht(e){const t=e.wait?.resolvedBy!==void 0,s=e.durationMs===void 0?"":`等了 ${R(e.durationMs)}`,n=t?` · ${s===""?"":`${s} → `}${re(e.wait?.resolvedBy??"")}`:" · 等待中…",a=[`${e.wait?.kind==="permission"?"PermissionRequest":"Notification"} 事件起止(hook 层观察)`,e.startTs===void 0?"":`开始:${e.startTs}`,t?`解除:${e.endTs??""}(事件 ${e.wait?.resolvedBy??""})`:"尚未解除"].filter(i=>i!=="").join(`
136
- `);return`<div class="tr waitrow" data-tip="${o(a)}">
137
- <span class="ic"></span>
138
- <span class="trlabel">⏸ ${o(e.label)}:${o(e.wait?.message??"")}${o(n)}</span>
139
- </div>`}function Ut(e,t,s){if(e.phase==="wait")return Ht(e);const n=e.steps.some(p=>p.endTs===void 0),a=e.durationMs===void 0?n?"…":"":R(e.durationMs),i=e.estTokens===void 0?"":` · 约 ${w(e.estTokens)}`,c=e.steps.find(p=>p.isError&&p.errorBrief!==void 0),u=e.failed&&c!==void 0&&!s?`<span class="failchip">${o(c.errorBrief??"")}</span>`:"",l=s?e.steps.map(p=>Dt(p)).join(""):"";return`<div class="tr${e.failed?" bad":n?" run":""}" data-act="node" data-key="${o(t)}" data-tip="${o(`阶段归类:工具类型规则(规则表公开);点击${s?"收起":"展开"}步骤${e.estTokens===void 0?"":`
140
- 约 token = 节点内步骤字节估算之和`}`)}">
141
- <span class="ic"></span>
142
- <span class="trlabel">${o(e.label)} · ${e.steps.length} 步</span>
143
- ${u}
144
- <span class="dur">${o(a)}${o(i)}</span>
145
- </div>${l===""?"":`<div class="steps">${l}</div>`}`}function $e(e,t,s){return`<div class="trace">${e.nodes.map((a,i)=>{const c=`${t}:${i}`;return Ut(a,c,s[c]===!0)}).join("")}</div>`}function Ft(e,t){return""}function qt(e,t,s,n){const a=de(e,s.todoOpen)&&t!==void 0&&t.nodes.length>0,i=t!==void 0&&t.nodes.length>0,c=e.status==="in_progress"&&i?a?"▾":"▸":at[e.status],u=rt(e,t),l=u===null?"":`<span class="ag" data-tip="${o(u==="子 agent"?`步骤归属含子代理(${e.agents.join(", ")});归属 = 时间窗 + Agent 跨度推断,归不准会进「其他」`:"步骤归属:主 agent(时间窗推断)")}">${o(u)}</span>`;return`<div class="td${e.status==="completed"?" done":e.status==="in_progress"?" doing":" todo"}">
146
- <div class="tdr"${i?` data-act="todo" data-id="${o(e.id)}"`:""}>
147
- <span class="mk${e.status==="in_progress"?" doing":""}">${c}</span>
148
- <span class="tdc">${e.status==="in_progress"?`<b>${o(e.content)}</b>`:o(e.content)}</span>
149
- ${l}
150
- ${Ft()}
151
- </div>
152
- ${a&&t?$e(t,e.id,s.nodeOpen):""}
153
- </div>`}function q(e,t){const s=t.traces.find(a=>a.todoId===null);if(!s||s.nodes.length===0)return"";const n=e.todoOpen.__other__!==!1;return`<div class="td doing other">
154
- <div class="tdr" data-act="todo" data-id="__other__" data-tip="归不到具体 todo 的步骤进「其他」桶(无时间戳/无进行中 todo/多任务并行归不准);推断规则如实降级,不硬塞">
155
- <span class="mk">${n?"▾":"▸"}</span>
156
- <span class="tdc">其他(归不准的步骤)</span>
157
- </div>
158
- ${n?$e(s,"__other__",e.nodeOpen):""}
159
- </div>`}function Bt(e,t){return`<div class="sec first todolist">
160
- <div class="cfgnote" data-tip="来源:assistant 文本里的 markdown 勾选清单(- [ ] / - [x]);会话没用工具清单时的降级展示,工具清单一旦出现立即让位">文本清单(agent 手写)</div>
161
- ${(e.textTodos??[]).map(n=>`<div class="td${n.checked?" done":" todo"}">
162
- <div class="tdr"><span class="mk">${n.checked?"✓":"○"}</span><span class="tdc">${o(n.content)}</span></div>
163
- </div>`).join("")}${q(t,e)}
164
- </div>`}function Kt(e,t,s){const n=s.narrative;let a="";n&&n.todos.length>0?a=`<div class="sec first todolist">${n.todos.map(u=>qt(u,n.traces.find(l=>l.todoId===u.id),s,e.harness)).join("")}${q(s,n)}</div>`:n&&(n.textTodos?.length??0)>0?a=Bt(n,s):n&&ie(n)?a=`<div class="sec first">
165
- <div class="cfgnote">${e.harness==="dsh"?"该 harness 不产生任务清单 —— 以下为执行轨迹":"本次会话未使用任务清单 —— 以下为执行轨迹"}</div>
166
- ${q(s,n)}
167
- </div>`:a=`<div class="sec first"><div class="livenone">○ 暂无叙事数据 —— 接线(<code>wingman hook install ${o(e.harness)}</code>)并跑一轮对话后,这里按任务展示执行轨迹</div></div>`;const i=`<div class="sec rawsec">
168
- <button class="rawhead" data-act="raw" data-tip="逐条 hook 事件原始流(0.2 的实况视图);默认折叠">${s.rawOpen?"▾":"▸"} 原始事件</button>
169
- <div class="rawbody${s.rawOpen?" open":""}">${Mt(e,t)}</div>
170
- </div>`;return a+i}function Wt(e,t){const s=t.narrative?.turnUsages??[],n=s.map(i=>Jt(i)).join("");return`<div class="sec first">
171
- <div class="label">每轮消耗 · usage 三字段为 harness 自报精确值</div>
172
- ${s.length>0?`<div class="ubox">${n}</div>`:'<div class="livenone">○ 本会话尚无 usage 数据 —— 接线并跑一轮对话后,这里逐轮列出精确消耗</div>'}
173
- </div>
174
- ${yt(e)}
175
- ${St(e)}`}function Jt(e){const t=e.ts===void 0?"":e.ts.slice(11,19),s=e.costUsd===void 0?"":`<span class="ucost">≈ ${o(le(e.costUsd))}</span>`,n=["usage 原文(harness 自报,精确):",`input ${e.inputTokens} · 缓存写 ${e.cacheCreationInputTokens} · 缓存读 ${e.cacheReadInputTokens} · output ${e.outputTokens}`,`本轮 context = 三字段之和 = ${e.contextTotal.value}`,e.costUsd===void 0?"$:该模型不在内置价目表,不换算(宁缺勿编)":"$ 按内置价目表换算(缓存读 ×0.1、缓存写 ×1.25/×2),标 ≈"].join(`
176
- `);return`<div class="urow" data-tip="${o(n)}">
177
- <span class="utime">${o(t)}</span>
178
- ${e.model===void 0?"":`<span class="chip">${o(e.model)}</span>`}
179
- <span class="unums">in ${o(w(e.inputTokens))} · 缓写 ${o(w(e.cacheCreationInputTokens))} · 缓读 ${o(w(e.cacheReadInputTokens))} · out ${o(w(e.outputTokens))}</span>
180
- <span class="utot">Σ ${o(w(e.contextTotal.value))}</span>
181
- ${s}
182
- </div>`}function Vt(e,t,s,n=null,a=me){const i=e.find(h=>h.harness===t)??e[0];if(!i)return`<div class="panel">${F()}</div>`;const c=i.__mock_note?`<span class="badge" data-tip="${o(i.__mock_note)}">MOCK 演示数据</span>`:"",u=i.harnessVersion?` ${i.harnessVersion}`:"",l=`${S[i.harness]}${u} · 扫描于 ${_e(i.scannedAt)}`,p=a.narrative,v=p&&ie(p)?Rt(p):"";return`<div class="panel">
183
- <div class="head">
184
- <span class="dot"></span>
185
- <span class="title">Wingman</span>
186
- <span class="meta">${o(l)}</span>
187
- ${c}
188
- <button class="collapse" data-act="toggle">收起 ▴</button>
189
- </div>
190
- ${Lt(n)}${wt(e,i.harness,ze(s.entries))}
191
- ${Pt(a,i.harness)}
192
- ${v}
193
- ${It(a)}
194
- <div class="pane${a.view==="tasks"?" act":""}">${Kt(i,s,a)}</div>
195
- <div class="pane${a.view==="control"?" act":""}">${Et(i)}</div>
196
- <div class="pane${a.view==="billing"?" act":""}">${Wt(i,a)}</div>
197
- <div class="suggslot">${Ct(a,i.harness)}</div>
198
- ${te(n)?"":`<div class="licfoot">${ve("licfootlink")}</div>`}
199
- </div>`}function Gt(e,t){const s=e.narratives?.[t]??null,n=C(s,e.sessionSel?.[t]);return{view:e.view??me.view,narrative:s!==null&&n!==null?s.states[n]??null:null,sessions:s?.sessions??[],selectedSession:n,sessionSeen:e.sessionSeen??{},suggHidden:e.suggHidden??{},island:e.island??!1,todoOpen:e.todoOpen??{},nodeOpen:e.nodeOpen??{},rawOpen:e.rawOpen??!1}}function Xt(e,t){if(e.error)return`<div class="wrap">${F(e.error)}</div>`;const s=e.configs.find(D=>D.harness===e.active)??e.configs[0];if(!s)return`<div class="wrap">${F()}</div>`;const n=Gt(e,s.harness),a=ne(e.live.entries,s.harness,s.capabilities.live,t),i=Y(s),c=i?`Σ ${y(i)} tok`:"Σ —",u=ae(e.narratives?.[s.harness]??null),l=dt(n.narrative,a,c,u,t),p=e.flow?Ot(e.flow):"",v=e.license??null,h=e.licenseOpen&&v?Nt(v):"",b=e.importResult?At(e.importResult):"",m=e.share?jt(e.share):"";return`<div class="wrap${e.open?" open":""}">
200
- ${bt(s,l)}
201
- ${Vt(e.configs,s.harness,e.live,v,n)}
202
- </div>${p}${h}${b}${m}`}const _=Ue(window.location.search);_&&document.documentElement.classList.add("island");const g={entries:[],connection:"connecting",replaying:!1},r={configs:[],active:"",open:!_,error:null,live:g,flow:null,license:null,licenseOpen:!1,importResult:null,share:null,view:"tasks",narratives:{},sessionSel:{},sessionSeen:{},suggHidden:{},island:_,todoOpen:{},nodeOpen:{},rawOpen:!1};let x=null,H=0;const J=document.querySelector("#app");function d(){J&&(J.innerHTML=Xt(r,Date.now()),_&&B())}function B(){let e=null;if(!r.open){const s=document.querySelector(".pill")??document.querySelector(".empty-card");if(s){const n=s.getBoundingClientRect();e={x:n.x,y:n.y,w:n.width,h:n.height}}}const t=Fe(r.open,e,x,Yt());window.location.hash!==t&&history.replaceState(null,"",t)}function Yt(){let e=null;for(const t of I){const s=ae(r.narratives?.[t]??null);if(!s)continue;const n=Date.parse(s.since);Number.isFinite(n)&&(e===null||n>e.sinceMs)&&(e={sinceMs:n,message:`${S[t]} 在等你:${s.message}`})}return e}function Qt(e){H+=1,x={target:e,seq:H},B();const t=H;setTimeout(()=>{x?.seq===t&&(x=null,B())},600)}function Zt(e){if(!Array.isArray(e))throw new Error("载荷不是 JSON 数组");for(const t of e){if(typeof t!="object"||t===null)throw new Error("载荷元素不是对象");const s=t;if(typeof s.harness!="string"||typeof s.scannedAt!="string")throw new Error("载荷元素缺少 harness/scannedAt 字段");const n=s.slots;for(const a of L)if(!n||!Array.isArray(n[a]))throw new Error(`harness ${s.harness} 缺少槽位 ${a}`)}return e}async function ge(){try{const e=await fetch("/api/anatomy");if(!e.ok)throw new Error(`GET /api/anatomy → HTTP ${e.status}`);const t=Zt(await e.json());r.configs=t,r.active=t.some(s=>s.harness===r.active)?r.active:t[0]?.harness??"",r.error=null}catch(e){r.error=e instanceof Error?e.message:String(e)}d()}async function zt(){try{const e=await fetch("/api/license");if(!e.ok)return;const t=ee(await e.json());t&&(r.license=t,d())}catch{}}function es(){const e=document.createElement("input");e.type="file",e.accept=".json,application/json",e.addEventListener("change",()=>{const t=e.files?.[0];t&&he(t)}),e.click()}async function he(e){let t;try{t=await e.text()}catch(s){r.importResult={ok:!1,message:`读取文件失败:${O(s)}`},d();return}await ts(t)}async function ts(e){let t;try{t=await fetch("/api/license/install",{method:"POST",headers:{"content-type":"application/json"},body:e})}catch(n){r.importResult={ok:!1,message:`导入请求失败:${O(n)}`},d();return}if(!t.ok){r.importResult={ok:!1,message:await A(t)},d();return}let s=null;try{s=ee(await t.json())}catch{s=null}s&&(r.license=s),r.importResult={ok:!0,message:Je(s)},d()}if(_)document.addEventListener("dragover",e=>e.preventDefault()),document.addEventListener("drop",e=>e.preventDefault());else{const e=document.createElement("div");e.id="drop",e.textContent="松开导入 license",document.body.appendChild(e);let t=0;const s=n=>Array.from(n.dataTransfer?.types??[]).includes("Files");document.addEventListener("dragenter",n=>{s(n)&&(n.preventDefault(),t+=1,e.style.display="flex")}),document.addEventListener("dragover",n=>{s(n)&&n.preventDefault()}),document.addEventListener("dragleave",n=>{s(n)&&(t=Math.max(0,t-1),t===0&&(e.style.display="none"))}),document.addEventListener("drop",n=>{if(!s(n))return;n.preventDefault(),t=0,e.style.display="none";const a=Array.from(n.dataTransfer?.files??[]),i=Ve(a.map(u=>u.name));if(i){r.importResult={ok:!1,message:i},d();return}const c=a[0];c&&he(c)})}const ss=300,U={};async function W(e){try{const t=await fetch(`/api/narrative?harness=${e}`);if(!t.ok)return;const s=nt(await t.json());if(s){r.narratives||(r.narratives={}),r.narratives[e]=s;const n=C(s,r.sessionSel?.[e]);n!==null&&(r.sessionSel||(r.sessionSel={}),r.sessionSel[e]=n,be(e,n)),d()}}catch{}}function be(e,t){const s=r.narratives?.[e]?.sessions.find(n=>n.id===t);s&&(r.sessionSeen||(r.sessionSeen={}),r.sessionSeen[`${e}:${t}`]=s.lastActivity)}function ns(e){U[e]===void 0&&(U[e]=window.setTimeout(()=>{delete U[e],W(e)},ss))}function as(){const e=r.configs.find(n=>n.harness===r.active)??r.configs[0];if(!e)return null;const t=r.narratives?.[e.harness]??null,s=C(t,r.sessionSel?.[e.harness]);return t===null||s===null?null:t.states[s]??null}function is(e){const t=r.todoOpen??{};let s;if(e==="__other__")s=t[e]!==!1;else{const n=as()?.todos.find(a=>a.id===e);s=n?de(n,t):t[e]===!0}r.todoOpen={...t,[e]:!s},d()}const I=["cc","codex","dsh"];function os(e){if(typeof e!="object"||e===null)return null;const t=e;if(!I.includes(t.harness)||typeof t.received_at!="string")return null;const s=t.payload;return typeof s!="object"||s===null||typeof s.hook_event_name!="string"?null:e}function we(e,t){g.entries=Qe(g.entries,t?{event:e,mock:t}:{event:e}),ns(e.harness),d()}function rs(){const e=new EventSource("/api/events");e.onopen=()=>{g.connection!=="open"&&(g.connection="open",d())},e.onmessage=t=>{try{const s=os(JSON.parse(t.data));s&&we(s)}catch{}},e.onerror=()=>{g.connection!=="retrying"&&(g.connection="retrying",d())}}const cs=200;async function ls(){if(!g.replaying){g.replaying=!0,d();try{const e=await fetch("/api/live-demo.json");if(!e.ok)throw new Error(`GET /api/live-demo.json → HTTP ${e.status}`);const t=await e.json();for(const s of t.events)we({harness:t.harness,received_at:new Date().toISOString(),payload:s},!0),await new Promise(n=>setTimeout(n,cs))}catch{}g.replaying=!1,d()}}let $=0;function P(){return r.configs.find(e=>e.harness===r.active)??r.configs[0]}function ds(e,t){for(const s of L){const n=(e.slots[s]??[]).find(a=>a.id===t);if(n)return n}}function ye(){$+=1,r.flow=null,d()}function O(e){return e instanceof Error?e.message:String(e)}async function A(e){const t=await e.text();try{const s=JSON.parse(t);if(typeof s.error=="string"&&s.error!=="")return s.error}catch{}return`HTTP ${e.status}${t?` — ${t}`:""}`}async function us(e,t){try{const s=await fetch("/api/action-plan-demo.json");if(!s.ok)throw new Error(`GET /api/action-plan-demo.json → HTTP ${s.status}`);const n=await s.json(),a=typeof n.__mock_note=="string"?n.__mock_note:"";if(!a.includes("MOCK"))throw new Error("演示件缺 MOCK 标注(增补条款 10)");const i=z(n.plan);if(e!==$)return;r.flow={phase:"confirm",title:Z(xe(i),i.action.enable),plan:i,mockNote:a}}catch(s){if(e!==$)return;r.flow={phase:"error",title:t,message:`服务器无动作端点(dev 模式?),且演示件不可用:${O(s)}`}}d()}async function V(e,t){const s=P();if(!s)return;const n=ds(s,e);if(!n)return;const a=t??!n.enabled,i=Z(n.nativeName,a),c=++$;r.flow={phase:"planning",title:i},d();const u={kind:"toggle",itemId:e,enable:a};let l=null;try{l=await fetch("/api/action/plan",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({harness:s.harness,action:u})})}catch{l=null}if(c===$){if(l===null||l.status===404||l.status===405){await us(c,i);return}if(!l.ok){r.flow={phase:"error",title:i,message:await A(l)},d();return}try{const p=z(await l.json());if(c!==$)return;r.flow={phase:"confirm",title:i,plan:p}}catch(p){if(c!==$)return;r.flow={phase:"error",title:i,message:O(p)}}d()}}async function ps(){if(r.flow?.phase!=="confirm")return;const{title:e,plan:t,mockNote:s}=r.flow;if(s){r.flow={phase:"demo",title:e},d();return}const n=++$;r.flow={phase:"applying",title:e},d();let a;try{a=await fetch("/api/action/apply",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({harness:t.harness,plan:t})})}catch(i){if(n!==$)return;r.flow={phase:"error",title:e,message:`apply 请求失败:${O(i)}`},d();return}if(n===$){if(a.status===403){const i=await A(a);if(n!==$)return;r.flow=He(e,i),d();return}if(!a.ok){r.flow={phase:"error",title:e,message:await A(a)},d();return}try{const i=De(await a.json());if(n!==$)return;i.ok?(r.flow={phase:"applied",title:e,backupDir:i.backupDir},ge()):r.flow={phase:"error",title:e,message:i.error}}catch(i){if(n!==$)return;r.flow={phase:"error",title:e,message:O(i)}}d()}}let E=0;function fs(){const e=P();e&&(E+=1,r.share={kind:"config",title:`分享配置样本 · ${S[e.harness]}`,lines:ft(e),phase:"preview"},d())}function vs(){E+=1,r.share={kind:"events",title:"分享事件样本",lines:vt(g.entries),phase:"preview"},d()}function ke(){E+=1,r.share=null,d()}function ms(e){r.share?.phase==="preview"&&(r.share={...r.share,lines:mt(r.share.lines,e)},d())}async function $s(){const e=r.share;if(e?.phase!=="preview"||e.lines.length===0||fe(e.kind,e.lines))return;const t=++E;r.share={...e,phase:"sending"},d();let s;try{s=await fetch(ut,{method:"POST",headers:{"content-type":"application/json"},body:ue(e.kind,e.lines)})}catch(n){if(t!==E)return;r.share={...e,phase:"error",message:`发送失败:${O(n)}`},d();return}if(t===E){if(s.ok)r.share={...e,phase:"sent"};else{const n=await A(s);if(t!==E)return;r.share={...e,phase:"error",message:n}}d()}}document.addEventListener("click",e=>{const t=e.target;if(!t)return;if(Be({island:_,open:r.open,inPanel:t.closest(".panel")!==null||t.closest(".pill")!==null||t.closest(".overlay")!==null,modalOpen:!!(r.flow||r.share||r.licenseOpen||r.importResult)})){r.open=!1,d();return}const s=t.closest('[data-act="locate"]');if(s instanceof HTMLElement&&s.dataset.target){Qt(s.dataset.target);return}const n=t.closest('[data-act="view"]');if(n instanceof HTMLElement&&n.dataset.view){r.view=n.dataset.view,d();return}const a=t.closest('[data-act="session"]');if(a instanceof HTMLElement&&a.dataset.id!==void 0){const m=P();m&&(r.sessionSel||(r.sessionSel={}),r.sessionSel[m.harness]=a.dataset.id,be(m.harness,a.dataset.id),d());return}const i=t.closest('[data-act="sugg-plan"]');if(i instanceof HTMLElement&&i.dataset.item!==void 0){V(i.dataset.item,i.dataset.enable==="1");return}const c=t.closest('[data-act="sugg-dismiss"]');if(c instanceof HTMLElement&&c.dataset.sugg!==void 0){const m=P();if(m){const D=r.narratives?.[m.harness]??null,Se=C(D,r.sessionSel?.[m.harness])??"";r.suggHidden||(r.suggHidden={}),r.suggHidden[`${m.harness}:${Se}:${c.dataset.sugg}`]=!0,d()}return}const u=t.closest('[data-act="todo"]');if(u instanceof HTMLElement&&u.dataset.id!==void 0){is(u.dataset.id);return}const l=t.closest('[data-act="node"]');if(l instanceof HTMLElement&&l.dataset.key!==void 0){const m=r.nodeOpen??{};r.nodeOpen={...m,[l.dataset.key]:m[l.dataset.key]!==!0},d();return}if(t.closest('[data-act="raw"]')){r.rawOpen=!r.rawOpen,d();return}if(t.closest('[data-act="toggle"]')){r.open=!r.open,d();return}if(t.closest('[data-act="demo"]')){ls();return}if(t.closest('[data-act="lic-open"]')){r.licenseOpen=!0,d();return}if(t.closest('[data-act="lic-close"]')){r.licenseOpen=!1,d();return}if(t.closest('[data-act="lic-import"]')){es();return}if(t.closest('[data-act="import-close"]')){r.importResult=null,d();return}if(t.closest('[data-act="share-config"]')){fs();return}if(t.closest('[data-act="share-events"]')){vs();return}const v=t.closest('[data-act="share-del"]');if(v instanceof HTMLElement&&v.dataset.i!==void 0){ms(Number(v.dataset.i));return}if(t.closest('[data-act="share-send"]')){$s();return}if(t.closest('[data-act="share-close"]')){ke();return}const h=t.closest('[data-act="tgl"]');if(h instanceof HTMLElement&&h.dataset.id){V(h.dataset.id);return}if(t.closest('[data-act="plan-confirm"]')){ps();return}if(t.closest('[data-act="plan-cancel"]')){ye();return}const b=t.closest("[data-tab]");b instanceof HTMLElement&&b.dataset.tab&&(r.active=b.dataset.tab,I.includes(b.dataset.tab)&&W(b.dataset.tab),d())});document.addEventListener("keydown",e=>{if(e.key==="Escape"){if(r.share){ke();return}if(r.importResult){r.importResult=null,d();return}if(r.licenseOpen){r.licenseOpen=!1,d();return}if(r.flow){r.flow.phase!=="applying"&&ye();return}_&&r.open&&(r.open=!1,d())}});const k=document.createElement("div");k.id="tip";document.body.appendChild(k);document.addEventListener("mousemove",e=>{const s=e.target?.closest("[data-tip]");if(!(s instanceof HTMLElement)||!s.dataset.tip){k.style.display="none";return}k.textContent=s.dataset.tip,k.style.display="block";const n=k.offsetWidth;let a=e.clientX+14;a+n>window.innerWidth-8&&(a=e.clientX-n-14),k.style.left=`${a}px`,k.style.top=`${e.clientY+14}px`});let G="";setInterval(()=>{const e=r.configs.find(n=>n.harness===r.active)??r.configs[0];if(!e||g.entries.length===0)return;const t=ne(g.entries,e.harness,e.capabilities.live,Date.now()),s=`${t.active}:${t.label}`;s!==G&&(G=s,d())},1e3);ge();zt();for(const e of I)W(e);rs();