claude-code-session-manager 0.108.0 → 0.109.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.
Files changed (63) hide show
  1. package/dist/assets/AgentLibrary-Br4Ispr4.js +2 -0
  2. package/dist/assets/{Button-DC6VzovQ.js → Button-C8Mc6MEP.js} +1 -1
  3. package/dist/assets/DataModel-Bz0MypYO.js +1 -0
  4. package/dist/assets/History-DT26Iy38.js +2 -0
  5. package/dist/assets/Hooks-e4xUGEf-.js +3 -0
  6. package/dist/assets/{Library-DLmnlfQt.js → Library-B3rzEhvN.js} +24 -24
  7. package/dist/assets/{ListDetail-YylbvUj_.js → ListDetail-BAvci_aP.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DMPKWHTF.js → MarkdownEditor-DBWTGgRz.js} +1 -1
  9. package/dist/assets/{McpServers-Da-Ty5nc.js → McpServers-C-grBN21.js} +2 -2
  10. package/dist/assets/{Memory-C3rUQ_cg.js → Memory-BmM0Fz-C.js} +5 -5
  11. package/dist/assets/{Panel-CC3JTnOI.js → Panel-Dc6F8wb4.js} +1 -1
  12. package/dist/assets/Permissions-m_NnldFh.js +3 -0
  13. package/dist/assets/Plugins-BB0TLUC9.js +2 -0
  14. package/dist/assets/ProvenanceBadge-k9PH0S2n.js +1 -0
  15. package/dist/assets/{SaveBar-C06rFT34.js → SaveBar-DSMnbUVI.js} +1 -1
  16. package/dist/assets/Scheduler-CbIcJEfM.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-Cp5mxblN.js → ScopeSwitcher-tHtloWbN.js} +1 -1
  18. package/dist/assets/Settings-CW3w7knp.js +6 -0
  19. package/dist/assets/{SkillReferenceGraph-DuWRmT_f.js → SkillReferenceGraph-Dndl1tzr.js} +14 -14
  20. package/dist/assets/Skills-CGnxroik.js +2 -0
  21. package/dist/assets/SystemPrompt-B3A3rHNp.js +1 -0
  22. package/dist/assets/TagLibrary-k4CTiAYo.js +1 -0
  23. package/dist/assets/{TiptapBody-1r4ODLJ7.js → TiptapBody-C2LfNQXw.js} +1 -1
  24. package/dist/assets/{Toggle-BGT68XHP.js → Toggle-CTXTU1q5.js} +1 -1
  25. package/dist/assets/{ViewTabs-T0NBc8uv.js → ViewTabs-CfEfOSE1.js} +1 -1
  26. package/dist/assets/historyTotals-Db0xy2Fg.js +1 -0
  27. package/dist/assets/{index-tr1w3hHB.css → index-C8Smiz7z.css} +1 -1
  28. package/dist/assets/{index-3_AtCAyd.js → index-fJxuoRvi.js} +697 -697
  29. package/dist/assets/listSkills-C1aWKf0T.js +1 -0
  30. package/dist/assets/{settingsSchema-CvoNk3Ye.js → settingsSchema-xxO9p4yF.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-DLS1_moj.js +1 -0
  32. package/dist/index.html +2 -2
  33. package/package.json +5 -2
  34. package/plugins/session-manager-dev/skills/develop/standards.md +5 -0
  35. package/scripts/README.md +7 -0
  36. package/src/main/agentLibrary.cjs +20 -9
  37. package/src/main/build-info.json +4 -4
  38. package/src/main/config.cjs +20 -10
  39. package/src/main/historyAggregator.cjs +46 -3
  40. package/src/main/index.cjs +2 -0
  41. package/src/main/ipcSchemas.cjs +2 -0
  42. package/src/main/lib/remoteE2eCrypto.cjs +109 -0
  43. package/src/main/pty.cjs +53 -0
  44. package/src/main/remoteClient.cjs +305 -0
  45. package/src/main/remoteClientIpc.cjs +101 -0
  46. package/src/main/webRemote.cjs +90 -108
  47. package/src/preload/api.d.ts +42 -5
  48. package/src/preload/index.cjs +27 -2
  49. package/dist/assets/AgentLibrary-Co54GThg.js +0 -2
  50. package/dist/assets/DataModel-DZ0vyJau.js +0 -1
  51. package/dist/assets/History-Df3YwpPe.js +0 -2
  52. package/dist/assets/Hooks-DplG6roH.js +0 -3
  53. package/dist/assets/Permissions-XvBiMj1-.js +0 -3
  54. package/dist/assets/Plugins-CzHY1mgS.js +0 -2
  55. package/dist/assets/ProvenanceBadge-DaJIWVcg.js +0 -1
  56. package/dist/assets/Scheduler-BehadU-0.js +0 -16
  57. package/dist/assets/Settings-Cg9LIni0.js +0 -4
  58. package/dist/assets/Skills-CMmCZfIv.js +0 -3
  59. package/dist/assets/SystemPrompt-D_-MV025.js +0 -1
  60. package/dist/assets/TagLibrary-D9Tv3yOJ.js +0 -1
  61. package/dist/assets/listSkills-QORduPIk.js +0 -1
  62. package/dist/assets/useAgentsDirWatch-1C2CBBya.js +0 -1
  63. package/dist/assets/useConfigDirWatch-Cd5PnTkB.js +0 -1
@@ -0,0 +1 @@
1
+ const p=a=>a instanceof Error?a.message:String(a);async function y(a,w={}){const d=w.nested??!0,s=[],c=async t=>{try{const r=await window.api.config.listDir(t,{dirsOnly:!0});return r.ok?r.entries:(s.push({path:t,error:r.error??"failed to list directory"}),[])}catch(r){return s.push({path:t,error:p(r)}),[]}};let o=0;const l=[],u=async t=>{var r;o>=6&&await new Promise(n=>l.push(n)),o++;try{return await window.api.config.readText(t)}catch(n){return s.push({path:t,error:p(n)}),null}finally{o--,(r=l.shift())==null||r()}};return{entries:(await Promise.all((await c(a)).map(async t=>{const r=`${t.path}/SKILL.md`,n=await u(r);return n!=null&&n.exists?[{name:t.name,path:r,dir:t.path,text:n.text}]:d?(await Promise.all((await c(t.path)).map(async e=>{const f=`${e.path}/SKILL.md`,i=await u(f);return i!=null&&i.exists?{name:e.name,path:f,dir:e.path,text:i.text}:null}))).filter(e=>e!==null):[]}))).flat(),errors:s}}export{y as l};
@@ -1 +1 @@
1
- import{aw as b,ah as n,S as N,aN as O}from"./index-3_AtCAyd.js";import{r as K}from"./claude-settings-schema-COigsajx.js";function G({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function V(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function D(){return $||($=B(K)),$}export{G as E,D as s,V as u};
1
+ import{aD as b,an as n,S as N,aW as O}from"./index-fJxuoRvi.js";import{r as K}from"./claude-settings-schema-COigsajx.js";function D({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=B(K)),$}export{D as E,V as s,G as u};
@@ -0,0 +1 @@
1
+ import{V as e,b0 as a,aX as n}from"./index-fJxuoRvi.js";const c=new Set(e.map(t=>t.tag));function l(t){return t.filter(o=>c.has(o))}function s(t,o={}){return{name:t.name,originalName:t.name,description:t.description??"",tools:t.tools,model:t.model??"inherit",effort:t.effort??void 0,color:t.color??"",tags:l(t.tags),projects:t.projects,action:t.action??void 0,actionLabel:t.actionLabel??void 0,title:t.title??void 0,body:t.body,...o}}function d(t,o){const i=a();n([i?`${i}/.claude/agents`:null,t?`${t}/.claude/agents`:null],o)}export{s as a,l as t,d as u};
package/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-3_AtCAyd.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-fJxuoRvi.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BsIlgG7c.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-Cu5D__1H.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-tr1w3hHB.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-C8Smiz7z.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.108.0",
3
+ "version": "0.109.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -67,7 +67,9 @@
67
67
  "lint:main-ts-check": "node scripts/check-main-ts-check.cjs",
68
68
  "lint:filenames": "node scripts/check-tracked-filenames.cjs",
69
69
  "lint:renderer-storage": "node scripts/check-renderer-storage.cjs",
70
- "lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:docs && npm run lint:paths && npm run lint:any && npm run lint:main-ts-check && npm run lint:filenames && npm run lint:renderer-storage",
70
+ "lint:test-smells": "node scripts/test-quality/smells.cjs --check tests/quality/smell-allowlist.json",
71
+ "lint:page-stewards": "node scripts/check-page-stewards.cjs",
72
+ "lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:docs && npm run lint:paths && npm run lint:any && npm run lint:main-ts-check && npm run lint:filenames && npm run lint:renderer-storage && npm run lint:test-smells && npm run lint:page-stewards",
71
73
  "sync:settings-schema": "node scripts/sync-settings-schema.cjs"
72
74
  },
73
75
  "keywords": [
@@ -147,6 +149,7 @@
147
149
  "@types/react": "^18.3.12",
148
150
  "@types/react-dom": "^18.3.1",
149
151
  "@vitejs/plugin-react": "^4.3.3",
152
+ "@vitest/coverage-v8": "4.1.6",
150
153
  "@xterm/addon-fit": "^0.10.0",
151
154
  "@xterm/addon-web-links": "^0.11.0",
152
155
  "@xterm/xterm": "^5.5.0",
@@ -39,6 +39,11 @@
39
39
  - Do not write production code without a test asserting the behavior it adds.
40
40
  - (Interactive sessions: the `test-driven-development` skill has the full red-green-refactor
41
41
  workflow. Headless PRD runs can't load it — the three rules above are the load-bearing core.)
42
+ - Put the red test in the nearest existing scenario test for that workflow.
43
+ - Create a new test file only for a new module or a new workflow.
44
+ - Name tests and files by behaviour, never by PRD id.
45
+ - Never assert on production source text; a static invariant is a `scripts/check-*.cjs` lint.
46
+ - Full rubric (what is bloat, how to delete safely): the `## Test value — good test vs bloat` section of `tests/README.md`.
42
47
 
43
48
  ## Visual design (UI/visual acceptance criteria)
44
49
 
package/scripts/README.md CHANGED
@@ -10,6 +10,8 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
10
10
  | --- | --- | --- |
11
11
  | audit-ops-hygiene.cjs | manual; named in `ops-sweep` SKILL.md | yes |
12
12
  | bench/main-bench.cjs | `npm run bench` (main-process micro-benches, plain Node); renderer half is `npm run bench:renderer` (`tests/bench/`, `vitest.bench.config.ts`, not in `test:unit`) | no |
13
+ | page-surface.cjs | manual and by the page steward personas `.claude/agents/page-*.md`; derives a page's owned/shared files, window.api methods, main handlers and tests | no |
14
+ | check-page-stewards.cjs | `npm run lint:page-stewards` (in `lint`) — every nav page has a valid `.claude/agents/page-<key>.md` steward persona; ratchets PENDING | no |
13
15
  | check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
14
16
  | check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
15
17
  | check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
@@ -32,6 +34,11 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
32
34
  | sm-ps.cjs | manual process-tree inspector | no |
33
35
  | sync-settings-schema.cjs | `npm run sync:settings-schema` | no |
34
36
  | write-build-info.cjs | npm `prepack` and `prepublishOnly` | no |
37
+ | test-quality/smells.cjs | `npm run lint:test-smells` (in `lint`) — static test-smell scan; ratchets source-text/no-assert against `tests/quality/smell-allowlist.json` | no |
38
+ | test-quality/coverage-map.cjs | manual, dev-only — per-test-file v8 coverage and unique-coverage attribution | no |
39
+ | test-quality/mutants.cjs | manual, dev-only — seeded mutation sampler; which test files kill which mutants | no |
40
+ | test-quality/coverage-guard.cjs | manual, dev-only — `--base <ref>`; fails if a test-only diff drops covered statements/branches | no |
41
+ | test-quality/report.cjs | manual, dev-only — keep/review/merge/bloat verdict per test file from the coverage map | no |
35
42
  | hooks/guard-destructive-git.cjs | `.claude/settings.json` PreToolUse + shim | yes |
36
43
  | hooks/guard-inline-implementation.cjs | `.claude/settings.json` PreToolUse + shim | yes |
37
44
  | hooks/guard-prd-writes.cjs | `.claude/settings.json` PreToolUse + shim | yes |
@@ -163,19 +163,30 @@ async function savePersona({
163
163
  return saveOverlay({ name, projectName, model, effort, loadSessions, validatePath, writeTextAtomic });
164
164
  }
165
165
  const target = validatePath(path.join(globalDir, `${name}.md`));
166
- // The Agent Library editor's form has no `seedVersion` field, so a save
167
- // through it never passes one explicitly — carry over whatever stamp the
168
- // existing file already has rather than silently erasing it. An explicit
169
- // `seedVersion` argument (e.g. the seeder's own future callers) still wins.
166
+ // A caller that omits seedVersion/title/projects/action/actionLabel (the
167
+ // Agent Library form has no seedVersion field) must not erase what the
168
+ // existing file holds — carry each undefined one over. An explicit value,
169
+ // including `projects: []`, still wins. `effort` is deliberately not
170
+ // carried: omitted means "write no effort line".
170
171
  let resolvedSeedVersion = seedVersion;
171
- if (resolvedSeedVersion === undefined) {
172
+ let resolvedTitle = title;
173
+ let resolvedProjects = projects;
174
+ let resolvedAction = action;
175
+ let resolvedActionLabel = actionLabel;
176
+ if ([seedVersion, title, projects, action, actionLabel].some((v) => v === undefined)) {
172
177
  try {
173
- const existing = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
174
- const existingVersion = Number(existing.fm.seedVersion);
175
- if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
178
+ const { fm } = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
179
+ if (resolvedSeedVersion === undefined) {
180
+ const existingVersion = Number(fm.seedVersion);
181
+ if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
182
+ }
183
+ if (resolvedTitle === undefined && fm.title) resolvedTitle = fm.title;
184
+ if (resolvedProjects === undefined && fm.projects) resolvedProjects = parseTools(fm.projects);
185
+ if (resolvedAction === undefined && fm.action) resolvedAction = decodeLine(fm.action);
186
+ if (resolvedActionLabel === undefined && fm.actionLabel) resolvedActionLabel = decodeLine(fm.actionLabel);
176
187
  } catch { /* no existing file (or unreadable) — nothing to carry over */ }
177
188
  }
178
- const text = serializePersona({ name, description, tools, model, effort, color, tags, projects, action, actionLabel, title, seedVersion: resolvedSeedVersion, body });
189
+ const text = serializePersona({ name, description, tools, model, effort, color, tags, projects: resolvedProjects, action: resolvedAction, actionLabel: resolvedActionLabel, title: resolvedTitle, seedVersion: resolvedSeedVersion, body });
179
190
  await writeTextAtomic(target, text);
180
191
  if (originalName && originalName !== name) {
181
192
  const oldReal = validatePath(path.join(globalDir, `${originalName}.md`));
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.108.0",
3
- "gitSha": "6abcc4e355e214ed60fe76291081712aa98d44c2",
4
- "gitShortSha": "6abcc4e3",
2
+ "version": "0.109.0",
3
+ "gitSha": "14869c8ae8d91422ea84f5b1e1fb483349544a80",
4
+ "gitShortSha": "14869c8a",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-10-09T19:19:08.109Z",
6
+ "builtAt": "2026-10-10T21:41:36.398Z",
7
7
  "dirty": false
8
8
  }
@@ -27,9 +27,9 @@ const { expandHome } = require('./lib/expandHome.cjs');
27
27
  const { listReferencedFiles } = require('./lib/importReferences.cjs');
28
28
  const atomicFs = require('./lib/atomicFs.cjs');
29
29
 
30
- /** Map<absPath, {watcher, refCount}> — one chokidar watcher per path. */
30
+ /** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
31
31
  const watchers = new Map();
32
- /** Map<senderId, Map<absPath, count>> — per-sender watch contributions for reload teardown. */
32
+ /** Map<senderId, Map<watchKey, count>> — per-sender watch contributions for reload teardown. */
33
33
  const senderWatches = new Map();
34
34
  let window = null;
35
35
 
@@ -380,26 +380,34 @@ function normalizePathKey(p) {
380
380
  return path.resolve(expandHome(p));
381
381
  }
382
382
 
383
+ /** Watcher-map key: the resolved path, plus a depth suffix for non-default (bounded) watches. */
384
+ function watchKeyOf(key, depth) {
385
+ return depth === undefined ? key : `${key}\u0000d${depth}`;
386
+ }
387
+
383
388
  /**
384
389
  * Watch one or more absolute paths. Each call increments a refcount per path.
385
390
  * Unwatch decrements; when refcount hits zero the underlying watcher is closed.
386
391
  * Fires IPC event `config:changed` with {path, mtimeMs, kind} on any change.
387
392
  */
388
- function watch(paths, senderId) {
393
+ function watch(paths, senderId, opts) {
394
+ const depth = opts?.depth;
389
395
  for (const rawPath of paths) {
390
396
  validatePath(expandHome(rawPath)); // security check — throws if out of bounds
391
397
  const key = normalizePathKey(rawPath);
398
+ const mapKey = watchKeyOf(key, depth);
392
399
  if (senderId != null) {
393
400
  if (!senderWatches.has(senderId)) senderWatches.set(senderId, new Map());
394
401
  const sm = senderWatches.get(senderId);
395
- sm.set(key, (sm.get(key) || 0) + 1);
402
+ sm.set(mapKey, (sm.get(mapKey) || 0) + 1);
396
403
  }
397
- const existing = watchers.get(key);
404
+ const existing = watchers.get(mapKey);
398
405
  if (existing) {
399
406
  existing.refCount++;
400
407
  continue;
401
408
  }
402
409
  const w = chokidar.watch(key, {
410
+ ...(depth === undefined ? {} : { depth }),
403
411
  ignoreInitial: true,
404
412
  persistent: true,
405
413
  awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
@@ -420,13 +428,13 @@ function watch(paths, senderId) {
420
428
  w.on('error', (err) => {
421
429
  console.warn('[config] watcher error for', key, err.message);
422
430
  });
423
- watchers.set(key, { watcher: w, refCount: 1 });
431
+ watchers.set(mapKey, { watcher: w, refCount: 1 });
424
432
  }
425
433
  }
426
434
 
427
- function unwatch(paths, senderId) {
435
+ function unwatch(paths, senderId, opts) {
428
436
  for (const rawPath of paths) {
429
- const key = normalizePathKey(rawPath);
437
+ const key = watchKeyOf(normalizePathKey(rawPath), opts?.depth);
430
438
  if (senderId != null) {
431
439
  const sm = senderWatches.get(senderId);
432
440
  if (sm) {
@@ -489,8 +497,8 @@ function registerConfigHandlers() {
489
497
  return { ok: false, error: e.message };
490
498
  }
491
499
  }));
492
- ipcMain.on('config:watch', (e, { paths }) => { try { s.configWatch.parse(paths); watch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
493
- ipcMain.on('config:unwatch', (e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
500
+ ipcMain.on('config:watch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); watch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
501
+ ipcMain.on('config:unwatch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); unwatch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
494
502
  }
495
503
 
496
504
  module.exports = {
@@ -498,6 +506,8 @@ module.exports = {
498
506
  registerConfigHandlers,
499
507
  closeAllWatchers,
500
508
  releaseWatchesForSender,
509
+ watch,
510
+ unwatch,
501
511
  // exported for tests / direct use
502
512
  readJson,
503
513
  readText,
@@ -6,6 +6,7 @@ const path = require('node:path');
6
6
  const os = require('node:os');
7
7
  const { schemas } = require('./ipcSchemas.cjs');
8
8
  const historyRollup = require('./lib/historyRollup.cjs');
9
+ const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
9
10
 
10
11
  const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
11
12
  const PARSE_BUDGET_MS = 2_000;
@@ -52,6 +53,47 @@ function resolvePricingKey(modelId) {
52
53
  // Shared implementation: lib/lruCache.cjs (re-exported below for existing callers).
53
54
  const { LRUCache } = require('./lib/lruCache.cjs');
54
55
 
56
+ // ── history:updated broadcast ─────────────────────────────────────────────────
57
+ // Open History pages re-fetch on this event. Throttled leading+trailing so a
58
+ // backfill burst announces its first and last write only.
59
+ const HISTORY_UPDATED_THROTTLE_MS = 5000;
60
+ let historyNotifyTimer = null;
61
+ let historyNotifyPending = false;
62
+
63
+ function broadcastHistoryUpdated() {
64
+ try {
65
+ const { BrowserWindow } = require('electron');
66
+ for (const win of BrowserWindow.getAllWindows()) sendIfAlive(win, 'history:updated', {});
67
+ } catch {
68
+ // Never let a failed broadcast break aggregation.
69
+ }
70
+ }
71
+
72
+ function notifyHistoryUpdated() {
73
+ if (historyNotifyTimer) {
74
+ historyNotifyPending = true;
75
+ return;
76
+ }
77
+ broadcastHistoryUpdated();
78
+ const arm = () => {
79
+ historyNotifyTimer = setTimeout(() => {
80
+ historyNotifyTimer = null;
81
+ if (historyNotifyPending) {
82
+ historyNotifyPending = false;
83
+ broadcastHistoryUpdated();
84
+ arm();
85
+ }
86
+ }, HISTORY_UPDATED_THROTTLE_MS);
87
+ if (historyNotifyTimer.unref) historyNotifyTimer.unref();
88
+ };
89
+ arm();
90
+ }
91
+
92
+ async function appendRollupAndNotify(entries) {
93
+ await historyRollup.appendRollupDays(entries);
94
+ if (entries.length) notifyHistoryUpdated();
95
+ }
96
+
55
97
  /**
56
98
  * Cache for parseJSONL results.
57
99
  * Entry shape: { mtimeMs: number, size: number, readOffset: number, inode: number, result: AggrResult }
@@ -574,7 +616,7 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
574
616
  }
575
617
 
576
618
  if (!dryRun) {
577
- await historyRollup.appendRollupDays(entries);
619
+ await appendRollupAndNotify(entries);
578
620
  }
579
621
  return { finalizedDates: partial ? [] : Array.from(dates), partial };
580
622
  }
@@ -741,7 +783,7 @@ async function refreshIntradayToday() {
741
783
 
742
784
  const entries = [];
743
785
  for (const b of buckets.values()) entries.push(...bucketToRollupLines(b));
744
- if (entries.length) await historyRollup.appendRollupDays(entries);
786
+ if (entries.length) await appendRollupAndNotify(entries);
745
787
 
746
788
  return { date: today, projectsUpdated: buckets.size, strategy: INTRADAY_STRATEGY };
747
789
  }
@@ -859,7 +901,7 @@ async function aggregate(req) {
859
901
  if (b.date < today) backfillEntries.push(...bucketToRollupLines(b));
860
902
  }
861
903
  if (backfillEntries.length) {
862
- await historyRollup.appendRollupDays(backfillEntries);
904
+ await appendRollupAndNotify(backfillEntries);
863
905
  }
864
906
 
865
907
  const rollupRows = rollupMapToRows(rollupMap, finalizedDates);
@@ -951,6 +993,7 @@ module.exports = {
951
993
  finalizeClosedDays,
952
994
  decodeCwd,
953
995
  refreshIntradayToday,
996
+ appendRollupAndNotify,
954
997
  computeIntradayBuckets,
955
998
  // exported for tests
956
999
  scanAggrLines,
@@ -129,6 +129,7 @@ const heapSnapshot = require('./heapSnapshot.cjs');
129
129
  const filesIpc = require('./files.cjs');
130
130
  const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
131
131
  const webRemote = require('./webRemote.cjs');
132
+ const { registerRemoteClientHandlers } = require('./remoteClientIpc.cjs');
132
133
  const { listExchanges } = require('./exchanges.cjs');
133
134
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
134
135
  const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
@@ -1098,6 +1099,7 @@ git.register(ipcMain);
1098
1099
  filesIpc.registerFilesHandlers();
1099
1100
  registerDocEditHandlers();
1100
1101
  webRemote.registerRemoteHandlers();
1102
+ registerRemoteClientHandlers(() => mainWindow);
1101
1103
  chatRunner.registerChatHandlers();
1102
1104
 
1103
1105
  // Direct in-process PRD authoring for a queued PromptTicket classified
@@ -139,6 +139,7 @@ const configListDir = z.object({
139
139
  });
140
140
 
141
141
  const configWatch = z.array(z.string().min(1).max(4096));
142
+ const configWatchOpts = z.object({ depth: z.number().int().min(0).max(2) }).strict().optional();
142
143
 
143
144
  const configParseImports = z.object({ path: z.string().min(1).max(4096) });
144
145
 
@@ -1211,6 +1212,7 @@ module.exports = {
1211
1212
  configWriteText,
1212
1213
  configListDir,
1213
1214
  configWatch,
1215
+ configWatchOpts,
1214
1216
  configParseImports,
1215
1217
  sessionsPayload,
1216
1218
  layoutEnvelope,
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ // E2E encryption helpers (P-256 ECDH + AES-256-GCM, ADR §5.2).
4
+ // Shared by the agent side (webRemote.cjs) and the desktop client role.
5
+
6
+ const crypto = require('node:crypto');
7
+
8
+ /**
9
+ * Generate a P-256 ECDH keypair. Public key is SPKI DER base64url; private key
10
+ * is PKCS8 DER base64url. Both are stored in web-remote.json at 0600.
11
+ */
12
+ function generateE2EKeyPair() {
13
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
14
+ namedCurve: 'P-256',
15
+ publicKeyEncoding: { type: 'spki', format: 'der' },
16
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
17
+ });
18
+ return {
19
+ e2ePrivateKey: privateKey.toString('base64url'),
20
+ e2ePublicKey: publicKey.toString('base64url'),
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Derive an AES-256-GCM session key from two P-256 public keys via ECDH + HKDF.
26
+ * @param myPrivateKeyB64 Agent's PKCS8 private key, base64url DER
27
+ * @param peerPublicKeyB64 Browser's SPKI public key, base64url DER
28
+ * @param deviceId Used as HKDF salt for domain separation
29
+ * @returns 32-byte Buffer (AES-256-GCM key)
30
+ */
31
+ function deriveSessionKey(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
32
+ const myPrivKey = crypto.createPrivateKey({
33
+ key: Buffer.from(myPrivateKeyB64, 'base64url'),
34
+ format: 'der',
35
+ type: 'pkcs8',
36
+ });
37
+ const peerPubKey = crypto.createPublicKey({
38
+ key: Buffer.from(peerPublicKeyB64, 'base64url'),
39
+ format: 'der',
40
+ type: 'spki',
41
+ });
42
+ const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
43
+ // HKDF: salt = deviceId bytes for domain separation, info = fixed protocol label.
44
+ const salt = Buffer.from(deviceId, 'utf8');
45
+ const info = Buffer.from('sm-e2e-v1', 'utf8');
46
+ return Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 32));
47
+ }
48
+
49
+ /**
50
+ * Derive a 6-digit Short Authentication String from the ECDH shared secret.
51
+ * Uses a separate HKDF info label ('sm-sas-v1') so the SAS is independent of
52
+ * the session key. Both sides compute the same value; user confirms they match.
53
+ */
54
+ function deriveSas(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
55
+ const myPrivKey = crypto.createPrivateKey({
56
+ key: Buffer.from(myPrivateKeyB64, 'base64url'),
57
+ format: 'der',
58
+ type: 'pkcs8',
59
+ });
60
+ const peerPubKey = crypto.createPublicKey({
61
+ key: Buffer.from(peerPublicKeyB64, 'base64url'),
62
+ format: 'der',
63
+ type: 'spki',
64
+ });
65
+ const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
66
+ const salt = Buffer.from(deviceId, 'utf8');
67
+ const info = Buffer.from('sm-sas-v1', 'utf8');
68
+ const sasBytes = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 3));
69
+ const sasNum = ((sasBytes[0] << 16) | (sasBytes[1] << 8) | sasBytes[2]) % 1_000_000;
70
+ return sasNum.toString().padStart(6, '0');
71
+ }
72
+
73
+ /**
74
+ * Encrypt a plaintext JSON string into an AES-256-GCM box.
75
+ * @returns { nonce: base64url, ciphertext: base64url } — the nonce is 12 random bytes
76
+ */
77
+ function encryptBox(plaintext, sessionKey) {
78
+ const nonce = crypto.randomBytes(12);
79
+ const cipher = crypto.createCipheriv('aes-256-gcm', sessionKey, nonce);
80
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
81
+ const tag = cipher.getAuthTag(); // 16-byte GCM authentication tag
82
+ return {
83
+ nonce: nonce.toString('base64url'),
84
+ // Append the GCM tag to the ciphertext so decryptBox can locate it.
85
+ ciphertext: Buffer.concat([encrypted, tag]).toString('base64url'),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Decrypt an AES-256-GCM box produced by encryptBox (or the browser's equivalent).
91
+ * @returns Decrypted UTF-8 string, or null if authentication fails.
92
+ */
93
+ function decryptBox(nonceB64, ciphertextB64, sessionKey) {
94
+ try {
95
+ const nonce = Buffer.from(nonceB64, 'base64url');
96
+ const data = Buffer.from(ciphertextB64, 'base64url');
97
+ if (data.length < 16) return null; // too short to contain tag
98
+ const tag = data.subarray(data.length - 16);
99
+ const ciphertext = data.subarray(0, data.length - 16);
100
+ const decipher = crypto.createDecipheriv('aes-256-gcm', sessionKey, nonce);
101
+ decipher.setAuthTag(tag);
102
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
103
+ return plain;
104
+ } catch {
105
+ return null; // authentication failed — drop the message
106
+ }
107
+ }
108
+
109
+ module.exports = { generateE2EKeyPair, deriveSessionKey, deriveSas, encryptBox, decryptBox };
package/src/main/pty.cjs CHANGED
@@ -111,6 +111,44 @@ class PtyManager {
111
111
  this.outBuffers = new Map();
112
112
  this.flushTimers = new Map(); // tabId -> Timeout
113
113
  this.window = null;
114
+ this.outputListeners = new Map(); // tabId -> Set<{onData,onExit}>
115
+ }
116
+
117
+ /**
118
+ * Observe a tab's coalesced output (the same string the renderer gets) and
119
+ * its exit from the main process. Returns an unsubscribe function.
120
+ * @param {string} tabId
121
+ * @param {(tabId: string, chunk: string) => void} onData
122
+ * @param {(tabId: string, info: {exitCode: number, signal?: number}) => void} [onExit]
123
+ */
124
+ addOutputListener(tabId, onData, onExit) {
125
+ const entry = { onData, onExit };
126
+ let set = this.outputListeners.get(tabId);
127
+ if (!set) {
128
+ set = new Set();
129
+ this.outputListeners.set(tabId, set);
130
+ }
131
+ set.add(entry);
132
+ return () => {
133
+ const cur = this.outputListeners.get(tabId);
134
+ if (!cur) return;
135
+ cur.delete(entry);
136
+ if (cur.size === 0) this.outputListeners.delete(tabId);
137
+ };
138
+ }
139
+
140
+ /** Fire every onExit listener for `tabId` once (errors isolated), then drop them. */
141
+ #notifyExit(tabId, info) {
142
+ const set = this.outputListeners.get(tabId);
143
+ this.outputListeners.delete(tabId);
144
+ if (!set) return;
145
+ for (const l of [...set]) {
146
+ try {
147
+ l.onExit?.(tabId, info);
148
+ } catch (err) {
149
+ console.warn('[pty] output listener onExit threw', err);
150
+ }
151
+ }
114
152
  }
115
153
 
116
154
  /** Send any pending buffered output for `tabId` now, and clear its timer. */
@@ -124,6 +162,16 @@ class PtyManager {
124
162
  if (pending) {
125
163
  this.outBuffers.delete(tabId);
126
164
  sendIfAlive(this.window, `pty:data:${tabId}`, pending);
165
+ const set = this.outputListeners.get(tabId);
166
+ if (set) {
167
+ for (const l of [...set]) {
168
+ try {
169
+ l.onData?.(tabId, pending);
170
+ } catch (err) {
171
+ console.warn('[pty] output listener onData threw', err);
172
+ }
173
+ }
174
+ }
127
175
  }
128
176
  }
129
177
 
@@ -328,6 +376,7 @@ class PtyManager {
328
376
  this.sessions.delete(tabId);
329
377
  this.buffers.delete(tabId);
330
378
  this.#dropOutput(tabId);
379
+ this.#notifyExit(tabId, { exitCode, signal });
331
380
  return;
332
381
  }
333
382
  // Flush any still-buffered output BEFORE the exit event so the renderer
@@ -349,6 +398,7 @@ class PtyManager {
349
398
  });
350
399
  }
351
400
  sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode, signal });
401
+ this.#notifyExit(tabId, { exitCode, signal });
352
402
  this.sessions.delete(tabId);
353
403
  this.buffers.delete(tabId);
354
404
  });
@@ -411,6 +461,9 @@ class PtyManager {
411
461
  this.sessions.delete(tabId);
412
462
  this.buffers.delete(tabId);
413
463
  this.#dropOutput(tabId);
464
+ // Listeners learn the tab is gone now; the suppressed exit event that
465
+ // follows finds none left, so each fires exactly once.
466
+ this.#notifyExit(tabId, { exitCode: 0, signal: undefined });
414
467
  }
415
468
  }
416
469