claude-code-session-manager 0.114.0 → 0.115.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 (40) hide show
  1. package/dist/assets/{AgentLibrary-DZFM7Lto.js → AgentLibrary-CjsZLl5e.js} +1 -1
  2. package/dist/assets/{Button-BJVFKdGH.js → Button-mj0vvcdc.js} +1 -1
  3. package/dist/assets/{DataModel-CebvcP_y.js → DataModel-CG6ycpfG.js} +1 -1
  4. package/dist/assets/{History-BwISEUhR.js → History-DDqLQiAN.js} +1 -1
  5. package/dist/assets/{Hooks-Dac3smYP.js → Hooks-dRje24Nk.js} +1 -1
  6. package/dist/assets/{Library-DTjsUr9W.js → Library-Dl71nylN.js} +1 -1
  7. package/dist/assets/{ListDetail-B1L5TbjS.js → ListDetail-DcAD_cuA.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-BFRhWZD4.js → MarkdownEditor-D7JTNOOp.js} +1 -1
  9. package/dist/assets/{McpServers-CBjGCMg0.js → McpServers-BbvbhemH.js} +1 -1
  10. package/dist/assets/{Memory-DdLi5fqe.js → Memory-dyz8ZU1e.js} +1 -1
  11. package/dist/assets/{Panel-BQkkkjex.js → Panel-BkfK-W74.js} +1 -1
  12. package/dist/assets/{Permissions-CaACwY0B.js → Permissions-BnjsXblr.js} +1 -1
  13. package/dist/assets/{Plugins-DJeBwVoh.js → Plugins-DSyTJtTp.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge--N5j8xiK.js → ProvenanceBadge-CQXSiPHq.js} +1 -1
  15. package/dist/assets/{RemoteMachines-WjWaDHEA.js → RemoteMachines-li6EBPAt.js} +1 -1
  16. package/dist/assets/{SaveBar-DFmOjb4_.js → SaveBar-0aiuy1L2.js} +1 -1
  17. package/dist/assets/{Scheduler-Miap6_aY.js → Scheduler-Ul4jiK7q.js} +1 -1
  18. package/dist/assets/{ScopeSwitcher-CiEujp4L.js → ScopeSwitcher-DR262W_R.js} +1 -1
  19. package/dist/assets/{Settings-UGpW4P0j.js → Settings-BwUcGuar.js} +1 -1
  20. package/dist/assets/{SkillReferenceGraph-BgyAnxGW.js → SkillReferenceGraph-fv2FJUNK.js} +1 -1
  21. package/dist/assets/{Skills-BQUpnNfx.js → Skills-BA2I-iRE.js} +1 -1
  22. package/dist/assets/{SystemPrompt-CQjOgHGT.js → SystemPrompt-BgvyrAKx.js} +1 -1
  23. package/dist/assets/{TagLibrary-CTVpoLl0.js → TagLibrary-B5rKEDVT.js} +1 -1
  24. package/dist/assets/{TiptapBody-BtOcrdRR.js → TiptapBody-CBxxEulT.js} +1 -1
  25. package/dist/assets/{Toggle-Chri4G19.js → Toggle-W944m375.js} +1 -1
  26. package/dist/assets/{ViewTabs-BIenDGVv.js → ViewTabs-BLQrBE2n.js} +1 -1
  27. package/dist/assets/{historyTotals-CX8mUxJY.js → historyTotals-BUeyjlNY.js} +1 -1
  28. package/dist/assets/{index-AVrhGmUO.js → index-t37UuEZu.js} +40 -40
  29. package/dist/assets/{settingsSchema-uqwlznbH.js → settingsSchema-HwiOXtC7.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-BaezRtWw.js → useAgentsDirWatch-D0rI9G_h.js} +1 -1
  31. package/dist/assets/{useRemotePeers-BuFbBqDt.js → useRemotePeers-BipAySOB.js} +1 -1
  32. package/dist/index.html +1 -1
  33. package/package.json +1 -1
  34. package/src/main/build-info.json +4 -4
  35. package/src/main/chatRunner.cjs +8 -2
  36. package/src/main/ipcSchemas.cjs +26 -1
  37. package/src/main/lib/remoteDrilldown.cjs +91 -0
  38. package/src/main/lib/remoteTranscriptTail.cjs +90 -0
  39. package/src/main/webRemote.cjs +90 -57
  40. package/src/preload/api.d.ts +3 -0
@@ -1 +1 @@
1
- import{aF as b,ap as n,S as N,aZ as O}from"./index-AVrhGmUO.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{aF as b,ap as n,S as N,aZ as O}from"./index-t37UuEZu.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 +1 @@
1
- import{V as e,b3 as a,a_ as n}from"./index-AVrhGmUO.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};
1
+ import{V as e,b3 as a,a_ as n}from"./index-t37UuEZu.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};
@@ -1 +1 @@
1
- import{ap as t,A as f,be as g,aF as d}from"./index-AVrhGmUO.js";function j({children:s,right:e}){return t.jsxs("div",{className:"flex items-baseline justify-between mb-2.5",children:[t.jsx("span",{className:"text-[11.5px] font-bold uppercase tracking-[0.8px] text-fg-faint",children:s}),e]})}function y({kind:s="soft",icon:e,children:c,onClick:i,disabled:o,testId:n}){return t.jsxs("button",{type:"button","data-testid":n,onClick:i,disabled:o,className:`shrink-0 inline-flex items-center gap-2 rounded-lg px-3.5 py-2 text-[13px] font-semibold transition-colors disabled:opacity-50 ${s==="soft"?"bg-bg-hi border border-line text-fg hover:bg-bg":"text-fg-dim hover:text-fg"}`,children:[e&&t.jsx(f,{name:e,size:15}),c]})}function N({icon:s,title:e,subtitle:c,badge:i,action:o,testId:n}){return t.jsxs("div",{"data-testid":n,className:"flex items-center gap-3.5 bg-bg-hi border border-line rounded-xl px-[18px] py-3.5",children:[t.jsx("span",{className:"w-[34px] h-[34px] rounded-lg bg-bg border border-line grid place-items-center text-fg-dim shrink-0",children:t.jsx(f,{name:s,size:17})}),t.jsxs("div",{className:"flex-1 min-w-0",children:[t.jsxs("div",{className:"flex items-center gap-2.5 mb-0.5",children:[t.jsx("span",{className:"text-sm font-semibold text-fg truncate",children:e}),i]}),t.jsx("div",{className:"font-mono text-[11.5px] text-fg-faint truncate",children:c})]}),o]})}function v({onClick:s,disabled:e,testId:c,children:i="Revoke"}){return t.jsx("button",{"data-testid":c,onClick:s,disabled:e,className:"shrink-0 rounded-lg border border-line bg-bg text-accent px-3.5 py-1.5 text-[12.5px] font-semibold hover:bg-bg-hi disabled:opacity-50 transition-colors",children:i})}function k({code:s,onConfirm:e,testId:c,codeTestId:i,confirmTestId:o,peer:n="browser"}){return t.jsxs("div",{"data-testid":c,className:"flex items-start gap-3.5 px-5 py-4 rounded-2xl border border-accent/40 bg-accent/[0.06]",children:[t.jsx("span",{className:"w-[38px] h-[38px] rounded-[11px] shrink-0 grid place-items-center bg-bg-hi text-accent",children:t.jsx(f,{name:"shield",size:20})}),t.jsxs("div",{className:"flex-1 min-w-0 text-sm text-fg",children:[t.jsx("strong",{className:"font-semibold",children:"Verify E2E session."})," Compare this code with the ",n," — they must match.",t.jsx("div",{"data-testid":i,className:"my-2 text-3xl font-mono tracking-[0.3em] text-center text-accent select-all",children:s}),t.jsx("p",{className:"text-[12.5px] text-fg-faint mb-3",children:"If the codes match, confirm below. Mutating commands are blocked until confirmed."}),t.jsx("button",{"data-testid":o,onClick:e,className:"rounded-lg bg-accent text-white px-4 py-2 text-[13px] font-semibold hover:opacity-90 transition-opacity",children:"Codes match — confirm"})]})]})}function C({children:s,testId:e}){return t.jsx("div",{"data-testid":e,className:"bg-bg-hi border border-line rounded-xl p-5 text-center text-[13.5px] text-fg-faint italic font-serif",children:s})}const m=s=>(s==null?void 0:s.message)||String(s);function S(s){const e=g(r=>r.show),[c,i]=d.useState([]),o=d.useRef(s);o.current=s;const n=d.useCallback(async()=>{try{i(await window.api.remoteClient.list())}catch(r){e("error",m(r))}},[e]);d.useEffect(()=>{n();const r=window.api.remoteClient.onStatus(a=>{var x;(x=o.current)==null||x.call(o,a),i(l=>l.map(b=>b.peerId===a.peerId?{...b,status:a.status,reason:a.reason,sas:a.sas}:b))});return()=>{r()}},[n]);const p=d.useCallback(async r=>{try{const a=await window.api.remoteClient.connect(r);i(x=>x.map(l=>l.peerId===r?{...l,status:a.status,reason:a.reason,sas:a.sas}:l))}catch(a){e("error",m(a))}},[e]),u=d.useCallback(async r=>{try{const a=await window.api.remoteClient.confirmSas(r);a.ok||e("error",a.reason??"SAS confirmation failed"),await n()}catch(a){e("error",m(a))}},[e,n]),h=d.useCallback(async r=>{try{await window.api.remoteClient.remove(r),await n()}catch(a){e("error",m(a))}},[e,n]);return{peers:c,refresh:n,connect:p,confirmSas:u,revoke:h}}export{C as E,y as P,N as R,k as S,v as a,j as b,S as u};
1
+ import{ap as t,A as f,be as g,aF as d}from"./index-t37UuEZu.js";function j({children:s,right:e}){return t.jsxs("div",{className:"flex items-baseline justify-between mb-2.5",children:[t.jsx("span",{className:"text-[11.5px] font-bold uppercase tracking-[0.8px] text-fg-faint",children:s}),e]})}function y({kind:s="soft",icon:e,children:c,onClick:i,disabled:o,testId:n}){return t.jsxs("button",{type:"button","data-testid":n,onClick:i,disabled:o,className:`shrink-0 inline-flex items-center gap-2 rounded-lg px-3.5 py-2 text-[13px] font-semibold transition-colors disabled:opacity-50 ${s==="soft"?"bg-bg-hi border border-line text-fg hover:bg-bg":"text-fg-dim hover:text-fg"}`,children:[e&&t.jsx(f,{name:e,size:15}),c]})}function N({icon:s,title:e,subtitle:c,badge:i,action:o,testId:n}){return t.jsxs("div",{"data-testid":n,className:"flex items-center gap-3.5 bg-bg-hi border border-line rounded-xl px-[18px] py-3.5",children:[t.jsx("span",{className:"w-[34px] h-[34px] rounded-lg bg-bg border border-line grid place-items-center text-fg-dim shrink-0",children:t.jsx(f,{name:s,size:17})}),t.jsxs("div",{className:"flex-1 min-w-0",children:[t.jsxs("div",{className:"flex items-center gap-2.5 mb-0.5",children:[t.jsx("span",{className:"text-sm font-semibold text-fg truncate",children:e}),i]}),t.jsx("div",{className:"font-mono text-[11.5px] text-fg-faint truncate",children:c})]}),o]})}function v({onClick:s,disabled:e,testId:c,children:i="Revoke"}){return t.jsx("button",{"data-testid":c,onClick:s,disabled:e,className:"shrink-0 rounded-lg border border-line bg-bg text-accent px-3.5 py-1.5 text-[12.5px] font-semibold hover:bg-bg-hi disabled:opacity-50 transition-colors",children:i})}function k({code:s,onConfirm:e,testId:c,codeTestId:i,confirmTestId:o,peer:n="browser"}){return t.jsxs("div",{"data-testid":c,className:"flex items-start gap-3.5 px-5 py-4 rounded-2xl border border-accent/40 bg-accent/[0.06]",children:[t.jsx("span",{className:"w-[38px] h-[38px] rounded-[11px] shrink-0 grid place-items-center bg-bg-hi text-accent",children:t.jsx(f,{name:"shield",size:20})}),t.jsxs("div",{className:"flex-1 min-w-0 text-sm text-fg",children:[t.jsx("strong",{className:"font-semibold",children:"Verify E2E session."})," Compare this code with the ",n," — they must match.",t.jsx("div",{"data-testid":i,className:"my-2 text-3xl font-mono tracking-[0.3em] text-center text-accent select-all",children:s}),t.jsx("p",{className:"text-[12.5px] text-fg-faint mb-3",children:"If the codes match, confirm below. Mutating commands are blocked until confirmed."}),t.jsx("button",{"data-testid":o,onClick:e,className:"rounded-lg bg-accent text-white px-4 py-2 text-[13px] font-semibold hover:opacity-90 transition-opacity",children:"Codes match — confirm"})]})]})}function C({children:s,testId:e}){return t.jsx("div",{"data-testid":e,className:"bg-bg-hi border border-line rounded-xl p-5 text-center text-[13.5px] text-fg-faint italic font-serif",children:s})}const m=s=>(s==null?void 0:s.message)||String(s);function S(s){const e=g(r=>r.show),[c,i]=d.useState([]),o=d.useRef(s);o.current=s;const n=d.useCallback(async()=>{try{i(await window.api.remoteClient.list())}catch(r){e("error",m(r))}},[e]);d.useEffect(()=>{n();const r=window.api.remoteClient.onStatus(a=>{var x;(x=o.current)==null||x.call(o,a),i(l=>l.map(b=>b.peerId===a.peerId?{...b,status:a.status,reason:a.reason,sas:a.sas}:b))});return()=>{r()}},[n]);const p=d.useCallback(async r=>{try{const a=await window.api.remoteClient.connect(r);i(x=>x.map(l=>l.peerId===r?{...l,status:a.status,reason:a.reason,sas:a.sas}:l))}catch(a){e("error",m(a))}},[e]),u=d.useCallback(async r=>{try{const a=await window.api.remoteClient.confirmSas(r);a.ok||e("error",a.reason??"SAS confirmation failed"),await n()}catch(a){e("error",m(a))}},[e,n]),h=d.useCallback(async r=>{try{await window.api.remoteClient.remove(r),await n()}catch(a){e("error",m(a))}},[e,n]);return{peers:c,refresh:n,connect:p,confirmSas:u,revoke:h}}export{C as E,y as P,N as R,k as S,v as a,j as b,S as u};
package/dist/index.html CHANGED
@@ -7,7 +7,7 @@
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-AVrhGmUO.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-t37UuEZu.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
13
  <link rel="stylesheet" crossorigin href="./assets/index-HpEbvDiE.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.114.0",
3
+ "version": "0.115.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",
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.114.0",
3
- "gitSha": "f126cf4dacd500ba8561b705d295b48d6601c008",
4
- "gitShortSha": "f126cf4d",
2
+ "version": "0.115.0",
3
+ "gitSha": "76b8ec0587142349394786fefc05b0cfa0ef9a66",
4
+ "gitShortSha": "76b8ec05",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-10-13T15:58:38.587Z",
6
+ "builtAt": "2026-10-13T18:38:06.307Z",
7
7
  "dirty": false
8
8
  }
@@ -966,8 +966,14 @@ function cancel(tabId) {
966
966
  // directly, reusing send()'s existing queued-vs-immediate branching rather
967
967
  // than duplicating it here.
968
968
 
969
- function enqueueExternalPrompt(tabId, prompt) {
970
- broadcast('chat:external-send', { tabId, prompt });
969
+ function enqueueExternalPrompt(tabId, prompt, extra = {}) {
970
+ const { sessionId, cwd } = extra ?? {};
971
+ broadcast('chat:external-send', {
972
+ tabId,
973
+ prompt,
974
+ ...(sessionId !== undefined ? { sessionId } : {}),
975
+ ...(cwd !== undefined ? { cwd } : {}),
976
+ });
971
977
  }
972
978
 
973
979
  // Asks the renderer to start a new Session (same launchSession a macro press
@@ -940,6 +940,21 @@ const chatExternalSend = z.object({
940
940
  ),
941
941
  });
942
942
 
943
+ // Phone drill-down reads. cwd is re-checked against the open tabs in
944
+ // webRemote.cjs; sessionKey becomes a transcript filename lookup so it is
945
+ // restricted to a session-id charset (no '/', so no traversal).
946
+ const remoteProjectSessions = z.object({ cwd: z.string().min(1).max(4096) });
947
+ const remoteSessionTranscript = z.object({
948
+ cwd: z.string().min(1).max(4096),
949
+ sessionKey: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/),
950
+ limit: z.number().int().min(1).max(100).optional(),
951
+ });
952
+ // One Epic's PRD list (slug/title/status projection only, never bodies).
953
+ const remoteSessionPrds = z.object({
954
+ cwd: z.string().min(1).max(4096),
955
+ epicId: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/),
956
+ });
957
+
943
958
  // Phone-initiated new Session. cwd/agentName are re-checked against the open
944
959
  // tabs / persona list in webRemote.cjs before anything touches the filesystem.
945
960
  const remoteSessionCreate = z.object({
@@ -1163,13 +1178,20 @@ const READ_COMMANDS = new Set([
1163
1178
  // Sensitive reads — return user data; require SAS confirmation same as MUTATE.
1164
1179
  const SAS_GATED_READS = new Set([
1165
1180
  'cmd:sessions:load',
1166
- // Same data as the event:session:list push (projects + their active Epics).
1181
+ // Same data the phone reads on demand (projects + their active Epics).
1167
1182
  'cmd:sessions:list',
1168
1183
  'cmd:history:aggregate',
1169
1184
  // subscribe initiates a live stream of session state/summary — sensitive.
1170
1185
  'cmd:session:subscribe',
1171
1186
  // Picker data (projects, personas, tags, macros) for the phone's New Session.
1172
1187
  'cmd:session:options',
1188
+ // Drill-down: project list, one project's Epics, one session's recent
1189
+ // transcript text — all user data, so SAS-gated.
1190
+ 'cmd:projects:list',
1191
+ 'cmd:project:sessions',
1192
+ 'cmd:session:transcript',
1193
+ // PRD slugs/titles/statuses for one Epic — project work content, so SAS-gated.
1194
+ 'cmd:session:prds',
1173
1195
  // NOTE: cmd:exchanges:list is intentionally NOT allowlisted — webRemote.cjs has
1174
1196
  // no dispatch handler for it, so an allowlist entry would only fail closed with
1175
1197
  // an opaque reject. Re-add here together with the handler when remote exchanges
@@ -1305,6 +1327,9 @@ module.exports = {
1305
1327
  chatClassifyTicket,
1306
1328
  chatExternalSend,
1307
1329
  remoteSessionCreate,
1330
+ remoteProjectSessions,
1331
+ remoteSessionTranscript,
1332
+ remoteSessionPrds,
1308
1333
  exchangesList,
1309
1334
  agentsSavePersona,
1310
1335
  macrosSave,
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ // Pure builders for the phone remote's drill-down lists: projects (open tabs),
4
+ // then one project's Epics in every lifecycle status. readIndex is injected
5
+ // (production passes epicMint's readActiveIndex) so this module stays pure.
6
+
7
+ const path = require('node:path');
8
+
9
+ const STATUS_RANK = { active: 0, proposed: 1, completed: 2 };
10
+
11
+ function readSessions(readIndex, cwd) {
12
+ try {
13
+ const sessions = readIndex(cwd)?.sessions;
14
+ return sessions && typeof sessions === 'object' ? sessions : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function tabTitle(tab) {
21
+ return tab.label || path.basename(tab.cwd);
22
+ }
23
+
24
+ function validTabs(tabs) {
25
+ return (Array.isArray(tabs) ? tabs : []).filter((t) => t && typeof t.cwd === 'string' && t.cwd);
26
+ }
27
+
28
+ function toMs(value) {
29
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
30
+ if (typeof value === 'string' && value) {
31
+ const ms = Date.parse(value);
32
+ return Number.isNaN(ms) ? null : ms;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function epicUpdatedAt(epic) {
38
+ for (const v of [epic.updatedAt, epic.completedAt, epic.createdAt]) {
39
+ if (v !== undefined && v !== null && v !== '') return toMs(v);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function listRemoteProjects(tabs, readIndex) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ for (const tab of validTabs(tabs)) {
48
+ if (seen.has(tab.cwd)) continue;
49
+ seen.add(tab.cwd);
50
+ const counts = { proposed: 0, active: 0, completed: 0 };
51
+ for (const epic of Object.values(readSessions(readIndex, tab.cwd))) {
52
+ if (epic && Object.prototype.hasOwnProperty.call(counts, epic.status)) counts[epic.status] += 1;
53
+ }
54
+ out.push({ cwd: tab.cwd, title: tabTitle(tab), mainTabId: tab.id ?? tab.sessionId, counts });
55
+ }
56
+ return out.sort((a, b) => a.title.localeCompare(b.title));
57
+ }
58
+
59
+ function listRemoteProjectSessions(tabs, readIndex, cwd, { completedLimit = 20 } = {}) {
60
+ const tab = validTabs(tabs).find((t) => t.cwd === cwd);
61
+ if (!tab) return null;
62
+ const project = { cwd: tab.cwd, title: tabTitle(tab), mainTabId: tab.id ?? tab.sessionId };
63
+ const rows = [];
64
+ for (const epic of Object.values(readSessions(readIndex, tab.cwd))) {
65
+ if (!epic || !Object.prototype.hasOwnProperty.call(STATUS_RANK, epic.status)) continue;
66
+ const firstLine = typeof epic.goalText === 'string' ? epic.goalText.split('\n')[0].trim().slice(0, 80).trim() : '';
67
+ rows.push({
68
+ epicId: epic.id,
69
+ claudeSessionId: typeof epic.claudeSessionId === 'string' && epic.claudeSessionId ? epic.claudeSessionId : null,
70
+ title: firstLine || epic.id,
71
+ status: epic.status,
72
+ tag: epic.tag,
73
+ agentType: epic.agentType,
74
+ spawnCwd: epic.worktree?.dir || epic.cwd || tab.cwd,
75
+ updatedAt: epicUpdatedAt(epic),
76
+ });
77
+ }
78
+ rows.sort((a, b) => {
79
+ const r = STATUS_RANK[a.status] - STATUS_RANK[b.status];
80
+ if (r !== 0) return r;
81
+ if (a.updatedAt === b.updatedAt) return 0;
82
+ if (a.updatedAt === null) return 1;
83
+ if (b.updatedAt === null) return -1;
84
+ return b.updatedAt - a.updatedAt;
85
+ });
86
+ let completed = 0;
87
+ const sessions = rows.filter((s) => s.status !== 'completed' || ++completed <= completedLimit);
88
+ return { project, sessions };
89
+ }
90
+
91
+ module.exports = { listRemoteProjects, listRemoteProjectSessions };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ // Bounded-tail reader for Claude Code JSONL transcripts: the last N human-readable
4
+ // chat messages (user prompts + assistant text) without reading the whole file.
5
+ // Top-level (no domain fits: epic/ transcripts are path-only; util/ is domain-free).
6
+ // Transcripts reach hundreds of MB, so cost is O(maxBytes), never O(file).
7
+
8
+ const fsp = require('node:fs/promises');
9
+
10
+ const DEFAULT_LIMIT = 30;
11
+ const MAX_LIMIT = 100;
12
+ const DEFAULT_MAX_BYTES = 524288;
13
+ const MAX_TEXT_CHARS = 4000;
14
+
15
+ /** Join the plain-text parts of a message `content` (string or block array). */
16
+ function textOf(content) {
17
+ if (typeof content === 'string') return content;
18
+ if (!Array.isArray(content)) return '';
19
+ return content
20
+ .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
21
+ .map((b) => b.text)
22
+ .join('\n');
23
+ }
24
+
25
+ function truncate(text) {
26
+ return text.length > MAX_TEXT_CHARS ? text.slice(0, MAX_TEXT_CHARS - 1) + '…' : text;
27
+ }
28
+
29
+ /** Parse one JSONL line into a chat message, or null when it is not one. */
30
+ function toChatMessage(line) {
31
+ let entry;
32
+ try {
33
+ entry = JSON.parse(line);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (!entry || typeof entry !== 'object') return null;
38
+ if (entry.isMeta || entry.isSidechain) return null;
39
+ const role = entry.type;
40
+ if (role !== 'user' && role !== 'assistant') return null;
41
+ const text = textOf(entry.message?.content).trim();
42
+ if (!text) return null;
43
+ return {
44
+ role,
45
+ text: truncate(text),
46
+ ts: typeof entry.timestamp === 'string' ? entry.timestamp : null,
47
+ uuid: typeof entry.uuid === 'string' ? entry.uuid : null,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * @param {string} filePath
53
+ * @param {{ limit?: number, maxBytes?: number }} [opts]
54
+ * @returns {Promise<Array<{ role: 'user'|'assistant', text: string, ts: string|null, uuid: string|null }>>}
55
+ * oldest first; [] on any read failure.
56
+ */
57
+ async function readRecentChatMessages(filePath, { limit = DEFAULT_LIMIT, maxBytes = DEFAULT_MAX_BYTES } = {}) {
58
+ if (typeof filePath !== 'string' || !filePath) return [];
59
+ const n = Math.floor(Number(limit));
60
+ const lim = Number.isFinite(n) ? Math.min(MAX_LIMIT, Math.max(1, n)) : DEFAULT_LIMIT;
61
+ const cap = Math.max(1, Math.floor(Number(maxBytes)) || DEFAULT_MAX_BYTES);
62
+ let fd;
63
+ try {
64
+ fd = await fsp.open(filePath, 'r');
65
+ const { size } = await fd.stat();
66
+ if (size === 0) return [];
67
+ const start = Math.max(0, size - cap);
68
+ // From mid-file, read one extra leading byte so the first split part is
69
+ // always a fragment (empty when start sits on a line boundary) — drop it.
70
+ const from = start > 0 ? start - 1 : 0;
71
+ const len = size - from;
72
+ const buf = Buffer.alloc(len);
73
+ const { bytesRead } = await fd.read(buf, 0, len, from);
74
+ const parts = buf.toString('utf8', 0, bytesRead).split('\n');
75
+ if (start > 0) parts.shift();
76
+ const out = [];
77
+ for (const line of parts) {
78
+ if (!line) continue;
79
+ const msg = toChatMessage(line);
80
+ if (msg) out.push(msg);
81
+ }
82
+ return out.slice(-lim);
83
+ } catch {
84
+ return [];
85
+ } finally {
86
+ await fd?.close().catch(() => {});
87
+ }
88
+ }
89
+
90
+ module.exports = { readRecentChatMessages };
@@ -436,10 +436,6 @@ async function connect() {
436
436
  broadcastStatus();
437
437
  }).catch(() => {});
438
438
  broadcastStatus();
439
- // v2: begin pushing the live session list once connected.
440
- // Reset diff-guard so the new client always receives a full session-list push.
441
- for (const c of _clients.values()) c.lastSessionListJson = null;
442
- startSessionListPush();
443
439
  });
444
440
 
445
441
  ws.on('message', (raw) => {
@@ -500,11 +496,9 @@ async function handleTokenRevoked(deviceId) {
500
496
  // The last completed assistant turn drives the Haiku summary (see maybeSummarize).
501
497
 
502
498
  const SESSION_POLL_MS = 1500;
503
- const SESSION_LIST_PUSH_MS = 5000;
504
499
  const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
505
500
 
506
501
  const _sessionWatchers = new Map(); // tabId → watcher
507
- let _sessionListTimer = null;
508
502
 
509
503
  /** Push an unsolicited event to ONE client, E2E-encrypted with its key and unicast via toConn. */
510
504
  function pushEventTo(client, type, payload) {
@@ -691,7 +685,6 @@ function stopSessionWatch(tabId) {
691
685
 
692
686
  function stopAllSessionWatches() {
693
687
  for (const tabId of Array.from(_sessionWatchers.keys())) stopSessionWatch(tabId);
694
- if (_sessionListTimer) { clearInterval(_sessionListTimer); _sessionListTimer = null; }
695
688
  }
696
689
 
697
690
  /**
@@ -729,47 +722,6 @@ function buildRemoteSessionList(tabs, readIndex) {
729
722
  return out;
730
723
  }
731
724
 
732
- /** Push the current session list (reuses sessionsStore — the canonical source). */
733
- async function pushSessionList() {
734
- try {
735
- // Honor the kill switch: when remote is disabled, push nothing (the project
736
- // list = cwds/titles is sensitive). dispatchEnvelope already blocks cmd:*;
737
- // this stops the unsolicited background push too.
738
- const cfg = await loadConfig();
739
- if (!cfg.remoteEnabled) return;
740
- // Don't push before SAS is confirmed — session cwds/titles are sensitive user data.
741
- // A relay that completes e2e:hello before the user confirms the SAS would otherwise
742
- // receive the full session list immediately (same threat SAS_GATED_READS blocks for
743
- // cmd:sessions:load). Guard here so client.lastSessionListJson is not poisoned either.
744
- if (!anyAuthenticated()) return;
745
- const sessionsStore = require('./sessionsStore.cjs');
746
- const data = await sessionsStore.load();
747
- // Tabs + their ACTIVE Epics → SessionMeta. tabId === claude sessionId so it
748
- // matches the transcript JSONL name used by cmd:session:subscribe.
749
- const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
750
- const sessions = buildRemoteSessionList(data?.tabs ?? [], readActiveIndex).map((s) => ({
751
- ...s,
752
- state: _sessionWatchers.get(s.tabId)?.state ?? null,
753
- }));
754
- const payload = { sessions, activeTabId: data?.activeTabId ?? null };
755
- const json = JSON.stringify(payload);
756
- for (const c of _clients.authenticated()) {
757
- if (json === c.lastSessionListJson) continue;
758
- c.lastSessionListJson = json;
759
- pushEventTo(c, 'event:session:list', payload);
760
- }
761
- } catch (e) {
762
- logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
763
- }
764
- }
765
-
766
- function startSessionListPush() {
767
- if (_sessionListTimer) return;
768
- pushSessionList().catch(() => {});
769
- _sessionListTimer = setInterval(() => pushSessionList().catch(() => {}), SESSION_LIST_PUSH_MS);
770
- if (typeof _sessionListTimer.unref === 'function') _sessionListTimer.unref();
771
- }
772
-
773
725
  // ─── SM-V2-03: mobile summary via Claude Haiku 4.5 ───────────────────────────
774
726
  // Summarization logic lives in lib/summarize.cjs (shared with exchanges.cjs).
775
727
 
@@ -811,14 +763,12 @@ async function maybeSummarize(w) {
811
763
  /**
812
764
  * Shared finish-up for any path that transitions a client's e2e state into 'authenticated'
813
765
  * (manual SAS confirmation or pinned-key auto-trust): log, broadcast the new
814
- * status, and flush the session list immediately rather than waiting for the
815
- * next SESSION_LIST_PUSH_MS tick.
766
+ * status, and tell that one client it may start requesting (event:remote:ready).
816
767
  */
817
768
  function finalizeE2eAuthenticated(logMessage, client) {
818
769
  logs.writeLine({ scope: 'webRemote', level: 'info', message: logMessage });
819
770
  broadcastStatus();
820
- if (client) client.lastSessionListJson = null;
821
- pushSessionList().catch(() => {});
771
+ if (client && loadConfigSync().remoteEnabled) pushEventTo(client, 'event:remote:ready', { ts: Date.now() });
822
772
  }
823
773
 
824
774
  // ─── Message handling & command dispatch ─────────────────────────────────────
@@ -1111,6 +1061,70 @@ function getDispatchMap() {
1111
1061
  };
1112
1062
  },
1113
1063
 
1064
+ 'cmd:projects:list': async () => {
1065
+ const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1066
+ const { listRemoteProjects } = require('./lib/remoteDrilldown.cjs');
1067
+ const data = await sessionsStore.load();
1068
+ return { projects: listRemoteProjects(data?.tabs ?? [], readActiveIndex) };
1069
+ },
1070
+
1071
+ 'cmd:project:sessions': async (payload) => {
1072
+ const parsed = schemas.remoteProjectSessions.parse(payload);
1073
+ const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1074
+ const { listRemoteProjectSessions } = require('./lib/remoteDrilldown.cjs');
1075
+ const data = await sessionsStore.load();
1076
+ const result = listRemoteProjectSessions(data?.tabs ?? [], readActiveIndex, parsed.cwd);
1077
+ if (!result) throw new Error('cwd is not an open project');
1078
+ return result;
1079
+ },
1080
+
1081
+ // cwd must be exactly an open tab's cwd; sessionKey is the tab's main id or
1082
+ // an Epic id / claudeSessionId from that project's active index.
1083
+ 'cmd:session:transcript': async (payload) => {
1084
+ const parsed = schemas.remoteSessionTranscript.parse(payload);
1085
+ const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1086
+ const { resolveEpicTranscriptPath } = require('./lib/epic/epicTranscriptPath.cjs');
1087
+ const { readRecentChatMessages } = require('./lib/remoteTranscriptTail.cjs');
1088
+ const data = await sessionsStore.load();
1089
+ const tab = (data?.tabs ?? []).find((t) => t && t.cwd === parsed.cwd);
1090
+ if (!tab) throw new Error('cwd is not an open project');
1091
+ validatePath(parsed.cwd);
1092
+ let filePath = null;
1093
+ if (parsed.sessionKey === (tab.id ?? tab.sessionId)) {
1094
+ filePath = require('./transcripts.cjs').transcriptPath(parsed.cwd, parsed.sessionKey);
1095
+ } else {
1096
+ const sessions = readActiveIndex(parsed.cwd)?.sessions ?? {};
1097
+ const epic = Object.values(sessions).find(
1098
+ (e) => e && (e.id === parsed.sessionKey || e.claudeSessionId === parsed.sessionKey),
1099
+ );
1100
+ if (epic && epic.claudeSessionId) {
1101
+ filePath = resolveEpicTranscriptPath({ cwd: parsed.cwd, claudeSessionId: epic.claudeSessionId }).path;
1102
+ }
1103
+ }
1104
+ if (!filePath) return { messages: [] };
1105
+ validatePath(filePath);
1106
+ return { messages: await readRecentChatMessages(filePath, { limit: parsed.limit }) };
1107
+ },
1108
+
1109
+ 'cmd:session:prds': async (payload) => {
1110
+ const parsed = schemas.remoteSessionPrds.parse(payload);
1111
+ const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1112
+ const data = await sessionsStore.load();
1113
+ const tab = (data?.tabs ?? []).find((t) => t && t.cwd === parsed.cwd);
1114
+ if (!tab) throw new Error('cwd is not an open project');
1115
+ validatePath(parsed.cwd);
1116
+ const sessions = readActiveIndex(parsed.cwd)?.sessions ?? {};
1117
+ const known = Object.values(sessions).some((e) => e && e.id === parsed.epicId) || !!sessions[parsed.epicId];
1118
+ if (!known) throw new Error('unknown epic');
1119
+ const all = await require('./scheduler.cjs').listPrdsInternal();
1120
+ const prds = all
1121
+ .filter((e) => e && e.epicId === parsed.epicId)
1122
+ .map((e) => ({ slug: e.slug, title: e.title, status: e.status ?? null, archived: !!e.archived }))
1123
+ .sort((a, b) => Number(a.archived) - Number(b.archived) || String(a.slug).localeCompare(String(b.slug)))
1124
+ .slice(0, 50);
1125
+ return { prds };
1126
+ },
1127
+
1114
1128
  'cmd:sessions:save': async (payload) => {
1115
1129
  const parsed = schemas.sessionsPayload.parse(payload);
1116
1130
  return sessionsStore.save(parsed);
@@ -1200,8 +1214,27 @@ function getDispatchMap() {
1200
1214
  // already runs inside the Electron main process (PRD 753).
1201
1215
  'cmd:chat:send': async (payload) => {
1202
1216
  const parsed = schemas.chatExternalSend.parse(payload);
1203
- chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
1204
- return { ok: true };
1217
+ const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1218
+ const data = await sessionsStore.load();
1219
+ const tabs = data?.tabs ?? [];
1220
+ // Resolve the target in main: an open tab, or an ACTIVE Epic (by id or
1221
+ // claudeSessionId) of an open project. Anything else fails loudly.
1222
+ if (tabs.some((t) => t.sessionId === parsed.tabId || t.id === parsed.tabId)) {
1223
+ chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
1224
+ return { ok: true };
1225
+ }
1226
+ for (const tab of tabs) {
1227
+ const sessions = Object.values(readActiveIndex(tab.cwd)?.sessions ?? {});
1228
+ const epic = sessions.find((e) => e && (e.id === parsed.tabId || e.claudeSessionId === parsed.tabId));
1229
+ if (!epic) continue;
1230
+ if (epic.status !== 'active' || !epic.claudeSessionId) throw new Error('target is not an active session');
1231
+ chatRunner.enqueueExternalPrompt(epic.id, parsed.prompt, {
1232
+ sessionId: epic.claudeSessionId,
1233
+ cwd: epic.cwd || tab.cwd,
1234
+ });
1235
+ return { ok: true };
1236
+ }
1237
+ throw new Error('unknown chat target');
1205
1238
  },
1206
1239
 
1207
1240
  'cmd:session:options': async () => {
@@ -1383,9 +1416,6 @@ function registerRemoteHandlers() {
1383
1416
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'failed to persist pinned peer pubkey', meta: { error: e?.message } });
1384
1417
  }
1385
1418
  }
1386
- // Flush the session list immediately — the push loop was suppressed while
1387
- // state !== 'authenticated', so the mobile app would otherwise wait up to
1388
- // SESSION_LIST_PUSH_MS for the first useful data.
1389
1419
  finalizeE2eAuthenticated('E2E session authenticated — SAS confirmed by user', target);
1390
1420
  return { ok: true };
1391
1421
  }));
@@ -1499,6 +1529,9 @@ module.exports = {
1499
1529
  setE2eAuthenticated: (key, clientKey = LEGACY_CLIENT_KEY) => {
1500
1530
  _clients.getOrCreate(clientKey).e2e = makeState('authenticated', key, null);
1501
1531
  },
1532
+ setConfigCache: (cfg) => { _configCache = { ...defaultConfig(), ...cfg }; _configCacheAt = Date.now(); },
1533
+ finalizeE2eAuthenticated,
1534
+ getClient: (clientKey = LEGACY_CLIENT_KEY) => _clients.getOrCreate(clientKey),
1502
1535
  revokeAllDevices,
1503
1536
  streamCount: () => _clients.values().reduce((n, c) => n + c.ptyStreams.size, 0),
1504
1537
  },
@@ -1598,6 +1598,9 @@ export interface ChatRunNoticeEvent {
1598
1598
  export interface ChatExternalSendEvent {
1599
1599
  tabId: string;
1600
1600
  prompt: string;
1601
+ /** Epic's claudeSessionId + project cwd, resolved by main so the renderer can deliver before the Epic's project is loaded. */
1602
+ sessionId?: string;
1603
+ cwd?: string;
1601
1604
  }
1602
1605
 
1603
1606
  export type CreateProjectFolderResult =