@evandu/super-pi 0.1.102 → 0.1.103

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "hash": "800da36325ed46c54e1bfd7c3f280a527dd42baefe4912bcef71b0c75531458b",
3
- "recordedAt": "2026-10-21T07:04:30.046Z"
2
+ "hash": "4582a716a58c8faa033bdb18d408a0ec92e70f6d6f1fdf03d539047e71587d04",
3
+ "recordedAt": "2026-10-21T08:51:19.447Z"
4
4
  }
@@ -1,8 +1,8 @@
1
1
  {
2
- "hash": "71617a24a30a02b331cd6be0d75fae095a2c73c7e8f909b627ff06de444ff9ee",
2
+ "hash": "b8deff533f84ef04833217897a1670a38ace531494f44b488da818198f0e74cb",
3
3
  "files": {
4
4
  "src/client/App.tsx": "f03962cde47d8c98ec59dad6ea365ee140d7768157b5894cbc6a4d377648bae7",
5
- "src/client/FilesPanel.tsx": "17c3e7ef10914a8836c209bc47a51ad06d73025feb755ee8f68bba8f79ce6d16",
5
+ "src/client/FilesPanel.tsx": "bdee7302c732c5fe3347f6a58a4770d5e390178417add191e04205aa745935d9",
6
6
  "src/client/GitPanel.tsx": "c44f3c73936af73f436ec6a8cd9ee50f5367dd6887f881fa133ec101fb99bbac",
7
7
  "src/client/HelpWindow.tsx": "6bc90fd5dc0783b16a07b75be64cc70df21991c1f16ea1fcd894f815e7b403f1",
8
8
  "src/client/KnowledgeFilePicker.tsx": "a97ef9004088e3b7e6f90159c04fd8ff151f068e04fd35a58c435f40f46af461",
@@ -24,5 +24,5 @@
24
24
  "src/client/types.ts": "c2d8b16e056173a3951d87f3e7b63dcc53dc9178a5e73fa3e48b2d1ffd4e93c7",
25
25
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
26
26
  },
27
- "recordedAt": "2026-10-21T08:26:29.793Z"
27
+ "recordedAt": "2026-10-21T13:33:05.700Z"
28
28
  }
@@ -17,12 +17,12 @@ Error generating stack: `+e.message+`
17
17
  `))});continue}if(e.includes(`|`)&&r+1<t.length&&Te.test(t[r+1])&&t[r+1].includes(`-`)){let i=Ne(e),a=Ne(t[r+1]).map(Pe),o=[];for(r+=2;r<t.length&&t[r].trim()&&t[r].includes(`|`);)o.push(Fe(Ne(t[r]),i.length).map(e=>be(e))),r+=1;n.push({type:`table`,head:i.map(e=>be(e)),align:a,rows:o});continue}if(Se.test(e)||Ce.test(e)){let e=Me(t,r);n.push(e.node),r=e.next;continue}let s=[];for(;r<t.length&&t[r].trim()&&!je(t[r]);)s.push(t[r]),r+=1;s.length||(s.push(t[r]),r+=1),n.push({type:`paragraph`,children:be(s.join(`
18
18
  `))})}return n}function je(e){return xe.test(e)||k.test(e)||A.test(e)||we.test(e)||Se.test(e)||Ce.test(e)||/^\s*\$\$/.test(e)}function Me(e,t){let n=Ce.test(e[t]),r=n?Ce.exec(e[t]):Se.exec(e[t]),i=r[1].length,a=[],o=t;for(;o<e.length;){let t=Se.exec(e[o]),r=Ce.exec(e[o]),s=n?r:t;if(!s)break;let c=s[1].length;if(c<i)break;let l=[s[s.length-1]];for(o+=1;o<e.length;){let n=e[o];if(!n.trim()){let t=e[o+1]??``;if(!t.trim()||t.search(/\S/)<=c)break;l.push(``),o+=1;continue}let i=n.search(/\S/);if(i===0&&t||i===0&&r)break;if(i>c)l.push(n.slice(Math.min(i,c+2))),o+=1;else if(i===c){if(je(n))break;l.push(n),o+=1}else break}a.push(Ae(l.join(`
19
19
  `)))}return{node:{type:`list`,ordered:n,start:n?Number(r[2]):1,items:a},next:o}}function Ne(e){let t=e.trim();t.startsWith(`|`)&&(t=t.slice(1)),t.endsWith(`|`)&&!t.endsWith(`\\|`)&&(t=t.slice(0,-1));let n=[],r=``;for(let e=0;e<t.length;e+=1)t[e]===`\\`&&t[e+1]===`|`?(r+=`|`,e+=1):t[e]===`|`?(n.push(r.trim()),r=``):r+=t[e];return n.push(r.trim()),n}function Pe(e){let t=e.startsWith(`:`),n=e.endsWith(`:`);return t&&n?`center`:n?`right`:t?`left`:null}function Fe(e,t){let n=e.slice(0,t);for(;n.length<t;)n.push(``);return n}var Ie={alpha:`α`,beta:`β`,gamma:`γ`,delta:`δ`,epsilon:`ε`,varepsilon:`ε`,zeta:`ζ`,eta:`η`,theta:`θ`,vartheta:`ϑ`,iota:`ι`,kappa:`κ`,lambda:`λ`,mu:`μ`,nu:`ν`,xi:`ξ`,pi:`π`,varpi:`ϖ`,rho:`ρ`,varrho:`ϱ`,sigma:`σ`,varsigma:`ς`,tau:`τ`,upsilon:`υ`,phi:`φ`,varphi:`ϕ`,chi:`χ`,psi:`ψ`,omega:`ω`,Gamma:`Γ`,Delta:`Δ`,Theta:`Θ`,Lambda:`Λ`,Xi:`Ξ`,Pi:`Π`,Sigma:`Σ`,Upsilon:`Υ`,Phi:`Φ`,Psi:`Ψ`,Omega:`Ω`},Le={times:`×`,div:`÷`,pm:`±`,mp:`∓`,cdot:`·`,ast:`∗`,star:`⋆`,circ:`∘`,bullet:`∙`,leq:`≤`,le:`≤`,geq:`≥`,ge:`≥`,neq:`≠`,ne:`≠`,approx:`≈`,sim:`∼`,simeq:`≃`,equiv:`≡`,propto:`∝`,ll:`≪`,gg:`≫`,in:`∈`,notin:`∉`,ni:`∋`,subset:`⊂`,subseteq:`⊆`,supset:`⊃`,supseteq:`⊇`,cup:`∪`,cap:`∩`,setminus:`∖`,emptyset:`∅`,varnothing:`∅`,aleph:`ℵ`,hbar:`ℏ`,ell:`ℓ`,Re:`ℜ`,Im:`ℑ`,sum:`∑`,prod:`∏`,coprod:`∐`,int:`∫`,iint:`∬`,iiint:`∭`,oint:`∮`,bigcup:`⋃`,bigcap:`⋂`,to:`→`,rightarrow:`→`,leftarrow:`←`,gets:`←`,leftrightarrow:`↔`,Rightarrow:`⇒`,Leftarrow:`⇐`,Leftrightarrow:`⇔`,mapsto:`↦`,longrightarrow:`⟶`,longleftarrow:`⟵`,uparrow:`↑`,downarrow:`↓`,ldots:`…`,cdots:`⋯`,vdots:`⋮`,ddots:`⋱`,dots:`…`,therefore:`∴`,because:`∵`,angle:`∠`,triangle:`△`,perp:`⊥`,parallel:`∥`,degree:`°`,prime:`′`,lbrace:`{`,rbrace:`}`,lbrack:`[`,rbrack:`]`,langle:`⟨`,rangle:`⟩`,lfloor:`⌊`,rfloor:`⌋`,lceil:`⌈`,rceil:`⌉`,vert:`|`,Vert:`‖`,backslash:`\\`,oplus:`⊕`,ominus:`⊖`,otimes:`⊗`,oslash:`⊘`,odot:`⊙`,dagger:`†`,ddagger:`‡`,quad:` `,qquad:` `,thinspace:` `,enspace:` `,negthinspace:``},Re=new Set(`arcsin.arccos.arctan.sinh.cosh.tanh.cot.sec.csc.sin.cos.tan.log.ln.lg.exp.lim.limsup.liminf.max.min.det.dim.ker.deg.gcd.hom.arg.Pr.sup.inf`.split(`.`)),ze={C:`ℂ`,H:`ℍ`,N:`ℕ`,P:`ℙ`,Q:`ℚ`,R:`ℝ`,Z:`ℤ`},Be={B:`ℬ`,E:`ℰ`,F:`ℱ`,H:`ℋ`,I:`ℐ`,L:`ℒ`,M:`ℳ`,R:`ℛ`,e:`ℯ`,g:`ℊ`,o:`ℴ`},Ve={mathbb:e=>ze[e]??e.toUpperCase(),mathcal:e=>Be[e]??e.toUpperCase(),mathscr:e=>Be[e]??e.toUpperCase()},He=new Set([`left`,`right`,`big`,`Big`,`bigg`,`Bigg`,`bigl`,`bigr`,`Bigl`,`Bigr`,`biggl`,`biggr`,`Biggl`,`Biggr`,`middle`,`displaystyle`,`textstyle`,`scriptstyle`,`limits`,`nolimits`,`nonumber`,`notag`,`label`]),Ue={hat:`^`,widehat:`^`,bar:`‾`,overline:`‾`,tilde:`~`,widetilde:`~`,vec:`→`,dot:`·`,ddot:`··`,check:`ˇ`,breve:`˘`,acute:`´`,grave:"`"},We=new Set([`(`,`)`,`[`,`]`,`|`,`/`,`<`,`>`]),Ge=new Set([`pmatrix`,`bmatrix`,`Bmatrix`,`vmatrix`,`Vmatrix`,`matrix`,`array`,`cases`,`aligned`,`align`,`alignat`,`split`,`gather`,`gathered`,`equation`,`smallmatrix`,`dcases`,`subarray`]);function Ke(e){let t=[],n=``,r=()=>{n&&=(t.push(...Xe(n)),``)},i=e=>{r(),t.push(e)};for(let a=0;a<e.length;){let o=e[a];if(o===`\\`){let t=a,r=/^\\([a-zA-Z]+|.)/.exec(e.slice(a));if(!r){n+=o,a+=1;continue}let s=r[1];if(a+=r[0].length,s===`\\`){i({type:`math-row-break`});continue}if(s===`{`||s===`}`){n+=s;continue}if(s===`,`||s===`;`||s===`:`||s===`!`||s===`&`)continue;if(s===`%`){for(;a<e.length&&e[a]!==`
20
- `;)a+=1;continue}if(He.has(s))continue;if(s===`begin`){let n=le(e,a),r=n.content,o=`\\end{${r}}`,s=e.indexOf(o,n.next),c=s===-1?e.length:s+o.length;i({type:`math-unsupported`,value:e.slice(t,c),reason:`\\begin{${r}} 不支持`}),a=c;continue}if(s===`frac`||s===`dfrac`||s===`tfrac`||s===`cfrac`){let t=Je(e,a),n=Je(e,t.next);a=n.next,i({type:`math-fraction`,numerator:Ke(t.content),denominator:Ke(n.content)});continue}if(s===`sqrt`){let t,n=a;if(e[n]===`[`){let r=qe(e,n);t=Ke(r.content),n=r.next}let r=Je(e,n);a=r.next,i({type:`math-root`,...t?{index:t}:{},radicand:Ke(r.content)});continue}if(s===`text`||s===`mathrm`||s===`textrm`||s===`mathbf`||s===`mathit`||s===`mathsf`||s===`mathtt`||s===`operatorname`||s===`textbf`){let t=le(e,a);a=t.next,i({type:`math-text`,value:t.content});continue}if(s in Ve){let t=Je(e,a);a=t.next;let n=Ve[s];i({type:`math-text`,value:[...t.content].map(n).join(``)});continue}if(s in Ue){let t=Je(e,a);a=t.next,i({type:`math-text`,value:`${Ze(Ke(t.content))}${Ue[s]}`});continue}if(s in Ie){i({type:`math-identifier`,value:Ie[s]});continue}if(s in Le){let e=Le[s];/^\d/.test(e)?i({type:`math-number`,value:e}):/^[A-Za-z]/.test(e)?i({type:`math-text`,value:e}):i({type:`math-operator`,value:e});continue}if(Re.has(s)){i({type:`math-function`,value:s});continue}if(Ge.has(s))continue;i({type:`math-unsupported`,value:`\\${s}`,reason:`\\${s} 不支持`})}else if(o===`{`){let n=le(e,a);t.push(...Ke(n.content)),a=n.next}else if(o===`}`||o===`&`||o===`$`)a+=1;else if(o===`^`||o===`_`){r();let n=Je(e,a+1);a=n.next,Ye(t,o===`^`?`sup`:`sub`,Ke(n.content))}else We.has(o)?(i({type:`math-text`,value:o}),a+=1):(n+=o,a+=1)}return r(),t}function qe(e,t){let n=e.indexOf(`]`,t+1);return n===-1?{content:``,next:e.length}:{content:e.slice(t+1,n),next:n+1}}function Je(e,t){let n=e[t];if(n===void 0)return{content:``,next:t};if(n===`{`)return le(e,t);let r=/^\\[a-zA-Z]+/.exec(e.slice(t));return r?{content:r[0],next:t+r[0].length}:{content:n,next:t+1}}function Ye(e,t,n){let r=e.pop();if(r){if(r.type===`math-script`)t===`sub`&&!r.sub?e.push({...r,sub:n}):t===`sup`&&!r.sup?e.push({...r,sup:n}):e.push(r);else if(r.type===`math-identifier`||r.type===`math-text`){let i=[...r.value],a=i.pop()??``;i.length&&e.push({type:r.type,value:i.join(``)}),e.push({type:`math-script`,base:[{type:`math-identifier`,value:a}],...t===`sub`?{sub:n}:{sup:n}})}else e.push({type:`math-script`,base:[r],...t===`sub`?{sub:n}:{sup:n}})}}function Xe(e){let t=[],n=0;for(;n<e.length;){let r=e.slice(n),i=/^\d+(?:\.\d+)?/.exec(r);if(i){t.push({type:`math-number`,value:i[0]}),n+=i[0].length;continue}let a=/^[A-Za-z]+/.exec(r);if(a){t.push(Re.has(a[0])?{type:`math-function`,value:a[0]}:{type:`math-identifier`,value:a[0]}),n+=a[0].length;continue}let o=/^[,;:!.'’|()\-+=<>/*]+/.exec(r);o?(t.push({type:`math-operator`,value:o[0]}),n+=o[0].length):(t.push({type:`math-text`,value:r[0]}),n+=1)}return t.reduce((e,t)=>{let n=e[e.length-1];return n?.type===`math-operator`&&t.type===`math-operator`?n.value+=t.value:e.push({...t}),e},[])}function Ze(e){return e.map(e=>{switch(e.type){case`math-text`:case`math-number`:case`math-identifier`:case`math-operator`:case`math-function`:return e.value;case`math-script`:return`${Ze(e.base)}${e.sub?`_{${Ze(e.sub)}}`:``}${e.sup?`^{${Ze(e.sup)}}`:``}`;case`math-fraction`:return`\\frac{${Ze(e.numerator)}}{${Ze(e.denominator)}}`;case`math-root`:return`\\sqrt{${Ze(e.radicand)}}`;case`math-row-break`:return`\\\\`;case`math-unsupported`:return e.value;default:return``}}).join(``)}function Qe({source:e}){let t=(0,p.useMemo)(()=>Ae(e),[e]);return(0,m.jsx)(`div`,{className:`md`,"data-md":`1`,children:t.map((e,t)=>tt(e,t))})}function $e({source:e}){let t=(0,p.useMemo)(()=>be(e),[e]);return(0,m.jsx)(`span`,{className:`md md-inline`,children:t.map((e,t)=>rt(e,t))})}function et(e){return ne(e).map((e,t)=>e.kind===`plain`?(0,m.jsx)(`span`,{children:e.text},t):(0,m.jsx)(`span`,{className:`code-token-${e.kind}`,children:e.text},t))}function tt(e,t){switch(e.type){case`paragraph`:return(0,m.jsx)(`p`,{className:`md-p`,children:e.children.map((e,t)=>rt(e,t))},t);case`heading`:{let n=`h${Math.min(6,Math.max(1,e.level))}`;return(0,m.jsx)(n,{className:`md-h md-h${e.level}`,children:e.children.map((e,t)=>rt(e,t))},t)}case`code-block`:return(0,m.jsx)(`pre`,{className:`md-pre`,"data-md-lang":e.lang||void 0,children:(0,m.jsx)(`code`,{className:`md-code`,children:et(e.value)})},t);case`list`:{let n=e.items.map((e,t)=>(0,m.jsx)(`li`,{className:`md-li`,children:e.map((e,t)=>tt(e,t))},t));return e.ordered?(0,m.jsx)(`ol`,{className:`md-ol`,start:e.start>1?e.start:void 0,children:n},t):(0,m.jsx)(`ul`,{className:`md-ul`,children:n},t)}case`blockquote`:return(0,m.jsx)(`blockquote`,{className:`md-quote`,children:e.children.map((e,t)=>tt(e,t))},t);case`rule`:return(0,m.jsx)(`hr`,{className:`md-rule`},t);case`table`:return(0,m.jsx)(`div`,{className:`md-table-wrap`,children:(0,m.jsxs)(`table`,{className:`md-table`,children:[(0,m.jsx)(`thead`,{children:(0,m.jsx)(`tr`,{children:e.head.map((t,n)=>(0,m.jsx)(`th`,{style:nt(e.align[n]),children:t.map((e,t)=>rt(e,t))},n))})}),(0,m.jsx)(`tbody`,{children:e.rows.map((t,n)=>(0,m.jsx)(`tr`,{children:t.map((t,n)=>(0,m.jsx)(`td`,{style:nt(e.align[n]),children:t.map((e,t)=>rt(e,t))},n))},n))})]})},t);case`math-block`:return(0,m.jsx)(it,{source:e.source},t);default:return null}}var nt=e=>e?{textAlign:e}:void 0;function rt(e,t){switch(e.type){case`text`:return(0,m.jsx)(p.Fragment,{children:e.value},t);case`code`:return(0,m.jsx)(`code`,{className:`md-code-inline`,children:e.value},t);case`strong`:return(0,m.jsx)(`strong`,{className:`md-strong`,children:e.children.map((e,t)=>rt(e,t))},t);case`em`:return(0,m.jsx)(`em`,{className:`md-em`,children:e.children.map((e,t)=>rt(e,t))},t);case`del`:return(0,m.jsx)(`del`,{className:`md-del`,children:e.children.map((e,t)=>rt(e,t))},t);case`break`:return(0,m.jsx)(`br`,{className:`md-break`},t);case`math`:return(0,m.jsx)(at,{source:e.source},t);case`link`:{let n=ae(e.href);return n?(0,m.jsx)(`a`,{className:`md-link`,href:n,title:e.title,target:`_blank`,rel:`noopener noreferrer nofollow`,children:e.children.map((e,t)=>rt(e,t))},t):(0,m.jsx)(p.Fragment,{children:e.children.map((e,t)=>rt(e,t))},t)}default:return null}}function it({source:e}){let t=(0,p.useMemo)(()=>Ke(e),[e]);return(0,m.jsx)(`div`,{className:`md-math md-math-block`,"data-md-math":`block`,"data-md-math-source":e,children:t.map((e,t)=>ot(e,t))})}function at({source:e}){let t=(0,p.useMemo)(()=>Ke(e),[e]);return(0,m.jsx)(`span`,{className:`md-math md-math-inline`,"data-md-math":`inline`,"data-md-math-source":e,children:t.map((e,t)=>ot(e,t))})}function ot(e,t){switch(e.type){case`math-text`:return(0,m.jsx)(`span`,{className:`md-m-text`,children:e.value},t);case`math-number`:return(0,m.jsx)(`span`,{className:`md-m-number`,children:e.value},t);case`math-identifier`:return(0,m.jsx)(`i`,{className:`md-m-var`,children:e.value},t);case`math-operator`:return(0,m.jsx)(`span`,{className:`md-m-op`,children:e.value},t);case`math-function`:return(0,m.jsx)(`span`,{className:`md-m-fn`,children:e.value},t);case`math-row-break`:return(0,m.jsx)(`br`,{className:`md-m-rowbreak`},t);case`math-unsupported`:return(0,m.jsx)(`span`,{className:`md-m-unsupported`,"data-md-math-unsupported":`1`,title:e.reason,children:e.value},t);case`math-script`:return(0,m.jsxs)(`span`,{className:`md-m-script`,children:[(0,m.jsx)(`span`,{className:`md-m-base`,children:e.base.map((e,t)=>ot(e,t))}),e.sup&&(0,m.jsx)(`sup`,{className:`md-m-sup`,children:e.sup.map((e,t)=>ot(e,t))}),e.sub&&(0,m.jsx)(`sub`,{className:`md-m-sub`,children:e.sub.map((e,t)=>ot(e,t))})]},t);case`math-fraction`:return(0,m.jsxs)(`span`,{className:`md-m-frac`,children:[(0,m.jsx)(`span`,{className:`md-m-num`,children:e.numerator.map((e,t)=>ot(e,t))}),(0,m.jsx)(`span`,{className:`md-m-den`,children:e.denominator.map((e,t)=>ot(e,t))})]},t);case`math-root`:return(0,m.jsxs)(`span`,{className:`md-m-root`,children:[(0,m.jsx)(`span`,{className:`md-m-radical`,children:`√`}),e.index&&(0,m.jsx)(`sup`,{className:`md-m-index`,children:e.index.map((e,t)=>ot(e,t))}),(0,m.jsx)(`span`,{className:`md-m-body`,children:e.radicand.map((e,t)=>ot(e,t))})]},t);default:return null}}var st=[[`help`,`Show help`],[`model`,`Choose model`],[`settings`,`Open settings`],[`clear`,`Clear session`],[`compact`,`Compact context`],[`reload`,`Reload agents`],[`new`,`New session`],[`fork`,`Fork session`],[`resume`,`Resume session`],[`stop`,`Stop run`],[`skill`,`Workspace skill`]].map(([e,t])=>({id:e,label:`/${e} — ${t}`,description:t})),ct=e=>e.normalize(`NFKD`).replace(/\p{M}/gu,``).toLowerCase();function lt(e,t){let n=ct(t.trim());return e.examples?.find(t=>ct(t).includes(n)&&t!==e.id)??e.id}function ut(e,t){let n=ct(t.trim());return n?e.filter(e=>ct(e.id).includes(n)||ct(e.label).includes(n)||e.examples?.some(e=>ct(e).includes(n))):e}function dt({query:e,anchorRect:t,command:n,workspaceId:r,onSelect:i,onClose:a}){let[o,s]=(0,p.useState)(0),[c,l]=(0,p.useState)(st),u=(0,p.useRef)(null),d=(0,p.useRef)([]);(0,p.useEffect)(()=>{let e=!1;if(r)return fetch(`/api/memory?workspaceId=${encodeURIComponent(r)}`).then(e=>e.ok?e.json():void 0).then(t=>{if(e||!t?.entries?.skills)return;let n=t.entries.skills.map(e=>{let t=e.content.split(/\r?\n/,1)[0].replace(/^#+\s*/,``).trim()||`unnamed-skill`,n=t.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g,`-`).replace(/^-|-$/g,``)||`unnamed-skill`;return{id:`skill:${n}`,label:t,description:`Workspace skill`,examples:[`skill:${n}`]}});l([...st.filter(e=>e.id!==`skill`),...n])}).catch(()=>{}),()=>{e=!0}},[r]);let f=ut([{id:`start`,label:`start — 打开浏览器`,description:`在浏览器中打开 Super Pi 控制台`,action:()=>n({type:`super_pi.start`})},{id:`restart`,label:`restart — 重启 Super Pi 服务`,description:`停掉并重新启动服务,加载磁盘上的新代码`,action:()=>n({type:`super_pi.restart`})},{id:`update`,label:`update — 更新 Super Pi`,description:`运行 npm install -g @evandu/super-pi 然后重启`,action:()=>n({type:`super_pi.update`})},...c],e),h=f.length===0?-1:Math.min(Math.max(o,0),f.length-1),g=e=>s(t=>f.length?Math.min(Math.max(t+e,0),f.length-1):0);(0,p.useEffect)(()=>{let e=e=>{u.current?.contains(e.target)||a()};return window.addEventListener(`mousedown`,e),()=>window.removeEventListener(`mousedown`,e)},[a]),(0,p.useEffect)(()=>{s(0)},[e]),(0,p.useLayoutEffect)(()=>{h>=0&&d.current[h]?.scrollIntoView({block:`nearest`})},[h]);let _={position:`fixed`,left:Math.max(8,Math.min(t.left,window.innerWidth-360)),bottom:Math.max(8,window.innerHeight-t.top+6),zIndex:200};return(0,m.jsx)(`div`,{ref:u,className:`slash-menu`,style:_,role:`listbox`,"aria-label":`斜杠命令`,onKeyDown:e=>{if(e.key===`Escape`)e.preventDefault(),a();else if(e.key===`ArrowDown`)e.preventDefault(),g(1);else if(e.key===`ArrowUp`)e.preventDefault(),g(-1);else if(e.key===`Home`)e.preventDefault(),s(0);else if(e.key===`End`)e.preventDefault(),s(f.length-1);else if(e.key===`Enter`){e.preventDefault();let t=f[h];if(!t)return;t.action?(t.action(),a()):i(t)}},children:f.length===0?(0,m.jsxs)(`div`,{className:`slash-menu-empty`,children:[`没有匹配「`,e,`」的命令`]}):f.map((e,t)=>(0,m.jsxs)(`button`,{ref:e=>{d.current[t]=e},type:`button`,role:`option`,"aria-selected":t===h,className:`slash-menu-item${t===h?` selected`:``}`,"data-slash-command":e.id,onClick:()=>{e.action?(e.action(),a()):i(e)},onMouseEnter:()=>s(t),children:[(0,m.jsxs)(`span`,{className:`slash-menu-item-id`,children:[`/`,e.id]}),(0,m.jsx)(`span`,{className:`slash-menu-item-sep`,"aria-hidden":`true`,children:` — `}),(0,m.jsx)(`span`,{className:`slash-menu-item-label`,children:e.label}),e.examples?.length?(0,m.jsx)(`span`,{className:`slash-menu-item-examples`,children:e.examples.slice(0,2).join(` · `)}):null]},e.id))})}var ft=4e3,pt=` `;function mt(e){return ne(e).map((e,t)=>e.kind===`plain`?(0,m.jsx)(`span`,{children:e.text},t):(0,m.jsx)(`span`,{className:`code-token-${e.kind}`,children:e.text},t))}function ht({value:e,onChange:t,onKeyDown:n,path:r}){return(0,m.jsxs)(`div`,{className:`file-textarea-highlight-wrap`,children:[(0,m.jsx)(`div`,{className:`file-textarea-highlight`,"data-file-text-highlight":r,"aria-hidden":`true`,children:mt(e)}),(0,m.jsx)(`textarea`,{className:`file-textarea`,"data-file-text":r,value:e,wrap:`off`,spellCheck:!1,autoCapitalize:`off`,autoCorrect:`off`,"aria-label":`编辑 ${r}`,onChange:e=>t(e.target.value),onKeyDown:n})]})}var gt=class extends Error{status;body;constructor(e,t){super(t.error||`HTTP ${e}`),this.status=e,this.body=t,this.name=`ApiError`}},_t=15e3,vt=e=>new gt(408,{error:`请求超时:${_t/1e3} 秒内服务端没有响应`,code:`timeout`,details:{path:e}});async function yt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),_t);try{let r=await fetch(e,{...t,signal:n.signal}),i;try{i=await r.json()}catch{i=void 0}if(!r.ok)throw new gt(r.status,i??{error:`HTTP ${r.status}`});return i}catch(t){throw n.signal.aborted?vt(e):t}finally{clearTimeout(r)}}var bt=e=>e instanceof gt?e:new gt(0,{error:e instanceof Error?e.message:`读取失败`}),xt=e=>encodeURIComponent(e),St=(e,t)=>`/api/files?workspaceId=${xt(e)}&path=${xt(t)}`,Ct=(e,t)=>`/api/file?workspaceId=${xt(e)}&path=${xt(t)}`,wt=(e,t)=>`/api/file?workspaceId=${xt(e)}&path=${xt(t)}&meta=1`,Tt=(e,t)=>`/api/files?workspaceId=${xt(e)}&q=${xt(t)}`,Et=e=>e<1024?`${e} B`:e<1048576?`${(e/1024).toFixed(1)} KB`:`${(e/1024/1024).toFixed(1)} MB`,Dt=e=>{let t=new Date(e);return Number.isNaN(t.getTime())?``:new Date(e).toLocaleTimeString(`zh-CN`,{hour:`2-digit`,minute:`2-digit`})};function Ot({workspaceId:e,entryPath:t,name:n,listings:r,expanded:i,toggle:a,openPaths:o,onOpen:s,onError:c,depth:l}){let u=r[t],d=i.has(t);return(0,p.useEffect)(()=>{if(!d||r[t]!==void 0)return;let n=!0;return yt(St(e,t)).then(e=>{n&&(r[t]=e)}).catch(e=>{n&&(r[t]={error:bt(e)})}),()=>{n=!1}},[d,t,e,r]),(0,m.jsxs)(`li`,{className:`file-node`,"data-file-dir":t,children:[(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-dir`,"data-file-dir-toggle":t,"aria-expanded":d,style:{paddingLeft:`${8+l*13}px`},onClick:()=>a(t),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`,children:d?`▾`:`▸`}),(0,m.jsx)(`span`,{className:`file-name`,children:n})]}),d&&(0,m.jsx)(`ul`,{className:`file-children`,children:u===void 0?(0,m.jsx)(`li`,{className:`file-row-loading`,"data-file-loading":t,children:`读取中…`}):`error`in u?(0,m.jsx)(`li`,{className:`file-row-error`,"data-file-error":t,children:u.error.message}):u.entries.length===0?(0,m.jsx)(`li`,{className:`file-row-empty`,"data-file-empty":t,children:`空目录`}):(0,m.jsxs)(m.Fragment,{children:[u.truncated&&(0,m.jsx)(`li`,{className:`file-row-empty`,"data-file-truncated":t,children:`条目过多,只显示前一部分`}),u.entries.map(t=>t.kind===`directory`?(0,m.jsx)(Ot,{workspaceId:e,entryPath:t.path,name:t.name,listings:r,expanded:i,toggle:a,openPaths:o,onOpen:s,onError:c,depth:l+1},t.path):(0,m.jsx)(kt,{entry:t,depth:l+1,openPaths:o,onOpen:s},t.path))]})})]})}function kt({entry:e,depth:t,openPaths:n,onOpen:r}){if(e.kind===`symlink`)return(0,m.jsx)(`li`,{className:`file-node`,"data-file-entry":e.path,"data-file-outside":e.path,children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,disabled:!0,title:`${e.path} 是一个指向 workspace 之外的符号链接,不能打开`,style:{paddingLeft:`${8+t*13}px`},children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),(0,m.jsx)(`span`,{className:`file-badge`,"data-file-outside-badge":`1`,children:`外部链接`})]})});let i=n.has(e.path);return(0,m.jsx)(`li`,{className:`file-node`,"data-file-entry":e.path,"data-file-row-open":i?`1`:`0`,children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,"data-file-open":e.path,disabled:!e.text,title:e.text?i?`${e.path}(已经打开,点击切换到它)`:e.path:`${e.name} 是二进制文件,无法以文本打开`,style:{paddingLeft:`${8+t*13}px`},onClick:()=>r(e),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),i&&(0,m.jsx)(`span`,{className:`file-open-mark`,"data-file-open-mark":e.path,title:`已经打开`,"aria-label":`已经打开`,children:`●`}),!e.text&&(0,m.jsx)(`span`,{className:`file-badge`,"data-file-binary":`1`,"aria-label":`不支持的文件类型`,children:`不支持`})]})})}function At({file:e,state:t,message:n,external:r,onChange:i,onSave:a,onReload:o,onOverwrite:s}){let[c,l]=(0,p.useState)(!0),u=/\.md$/i.test(e.path);return(0,m.jsxs)(`div`,{className:`file-editor`,"data-file-editor":e.path,"data-save-state":t,children:[(0,m.jsxs)(`div`,{className:`file-editor-head`,children:[(0,m.jsx)(`span`,{className:`file-editor-path`,title:e.path,children:e.path}),(0,m.jsxs)(`span`,{className:`file-editor-flags`,children:[u&&(0,m.jsx)(`button`,{type:`button`,className:`file-preview-toggle`,onClick:()=>l(e=>!e),children:c?`原文`:`预览`}),e.dirty&&(0,m.jsx)(`span`,{className:`file-dirty`,"data-file-dirty":`1`,children:`未保存`}),(0,m.jsx)(`button`,{type:`button`,className:`file-save`,"data-file-save":e.path,onClick:a,disabled:t===`saving`,children:t===`saving`?`保存中…`:`保存`})]})]}),r&&t!==`conflict`&&(0,m.jsxs)(`div`,{className:`file-conflict`,"data-file-external":e.path,role:`alert`,children:[(0,m.jsx)(`p`,{children:`这个文件在磁盘上已经被改过了,你的修改还没有覆盖它。`}),(0,m.jsxs)(`div`,{className:`file-conflict-actions`,children:[(0,m.jsx)(`button`,{type:`button`,"data-file-reload":e.path,onClick:o,children:`丢掉我的修改,读磁盘版本`}),(0,m.jsx)(`button`,{type:`button`,"data-file-overwrite":e.path,onClick:s,children:`仍然覆盖磁盘`})]})]}),t===`conflict`&&(0,m.jsxs)(`div`,{className:`file-conflict`,"data-file-conflict":e.path,role:`alert`,children:[(0,m.jsx)(`p`,{children:`保存被拒绝:这个文件在磁盘上已经被改过了,覆盖会丢掉对方的改动。`}),(0,m.jsxs)(`div`,{className:`file-conflict-actions`,children:[(0,m.jsx)(`button`,{type:`button`,"data-file-reload":e.path,onClick:o,children:`丢掉我的修改,读磁盘版本`}),(0,m.jsx)(`button`,{type:`button`,"data-file-overwrite":e.path,onClick:s,children:`仍然覆盖磁盘`})]})]}),n&&t!==`conflict`&&!r&&(0,m.jsx)(`p`,{className:`file-editor-message file-message-${t}`,"data-file-message":t,role:`status`,children:n}),u&&c?(0,m.jsx)(`div`,{className:`file-markdown-preview`,children:(0,m.jsx)(Qe,{source:e.content})}):(0,m.jsx)(ht,{value:e.content,onChange:i,onKeyDown:e=>{let t=e.currentTarget;if(e.nativeEvent.isComposing||e.keyCode===229)return;if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`s`){e.preventDefault(),a();return}if(e.key!==`Tab`)return;e.preventDefault();let{selectionStart:n,selectionEnd:r,value:o}=t;if(!o.slice(n,r).includes(`
20
+ `;)a+=1;continue}if(He.has(s))continue;if(s===`begin`){let n=le(e,a),r=n.content,o=`\\end{${r}}`,s=e.indexOf(o,n.next),c=s===-1?e.length:s+o.length;i({type:`math-unsupported`,value:e.slice(t,c),reason:`\\begin{${r}} 不支持`}),a=c;continue}if(s===`frac`||s===`dfrac`||s===`tfrac`||s===`cfrac`){let t=Je(e,a),n=Je(e,t.next);a=n.next,i({type:`math-fraction`,numerator:Ke(t.content),denominator:Ke(n.content)});continue}if(s===`sqrt`){let t,n=a;if(e[n]===`[`){let r=qe(e,n);t=Ke(r.content),n=r.next}let r=Je(e,n);a=r.next,i({type:`math-root`,...t?{index:t}:{},radicand:Ke(r.content)});continue}if(s===`text`||s===`mathrm`||s===`textrm`||s===`mathbf`||s===`mathit`||s===`mathsf`||s===`mathtt`||s===`operatorname`||s===`textbf`){let t=le(e,a);a=t.next,i({type:`math-text`,value:t.content});continue}if(s in Ve){let t=Je(e,a);a=t.next;let n=Ve[s];i({type:`math-text`,value:[...t.content].map(n).join(``)});continue}if(s in Ue){let t=Je(e,a);a=t.next,i({type:`math-text`,value:`${Ze(Ke(t.content))}${Ue[s]}`});continue}if(s in Ie){i({type:`math-identifier`,value:Ie[s]});continue}if(s in Le){let e=Le[s];/^\d/.test(e)?i({type:`math-number`,value:e}):/^[A-Za-z]/.test(e)?i({type:`math-text`,value:e}):i({type:`math-operator`,value:e});continue}if(Re.has(s)){i({type:`math-function`,value:s});continue}if(Ge.has(s))continue;i({type:`math-unsupported`,value:`\\${s}`,reason:`\\${s} 不支持`})}else if(o===`{`){let n=le(e,a);t.push(...Ke(n.content)),a=n.next}else if(o===`}`||o===`&`||o===`$`)a+=1;else if(o===`^`||o===`_`){r();let n=Je(e,a+1);a=n.next,Ye(t,o===`^`?`sup`:`sub`,Ke(n.content))}else We.has(o)?(i({type:`math-text`,value:o}),a+=1):(n+=o,a+=1)}return r(),t}function qe(e,t){let n=e.indexOf(`]`,t+1);return n===-1?{content:``,next:e.length}:{content:e.slice(t+1,n),next:n+1}}function Je(e,t){let n=e[t];if(n===void 0)return{content:``,next:t};if(n===`{`)return le(e,t);let r=/^\\[a-zA-Z]+/.exec(e.slice(t));return r?{content:r[0],next:t+r[0].length}:{content:n,next:t+1}}function Ye(e,t,n){let r=e.pop();if(r){if(r.type===`math-script`)t===`sub`&&!r.sub?e.push({...r,sub:n}):t===`sup`&&!r.sup?e.push({...r,sup:n}):e.push(r);else if(r.type===`math-identifier`||r.type===`math-text`){let i=[...r.value],a=i.pop()??``;i.length&&e.push({type:r.type,value:i.join(``)}),e.push({type:`math-script`,base:[{type:`math-identifier`,value:a}],...t===`sub`?{sub:n}:{sup:n}})}else e.push({type:`math-script`,base:[r],...t===`sub`?{sub:n}:{sup:n}})}}function Xe(e){let t=[],n=0;for(;n<e.length;){let r=e.slice(n),i=/^\d+(?:\.\d+)?/.exec(r);if(i){t.push({type:`math-number`,value:i[0]}),n+=i[0].length;continue}let a=/^[A-Za-z]+/.exec(r);if(a){t.push(Re.has(a[0])?{type:`math-function`,value:a[0]}:{type:`math-identifier`,value:a[0]}),n+=a[0].length;continue}let o=/^[,;:!.'’|()\-+=<>/*]+/.exec(r);o?(t.push({type:`math-operator`,value:o[0]}),n+=o[0].length):(t.push({type:`math-text`,value:r[0]}),n+=1)}return t.reduce((e,t)=>{let n=e[e.length-1];return n?.type===`math-operator`&&t.type===`math-operator`?n.value+=t.value:e.push({...t}),e},[])}function Ze(e){return e.map(e=>{switch(e.type){case`math-text`:case`math-number`:case`math-identifier`:case`math-operator`:case`math-function`:return e.value;case`math-script`:return`${Ze(e.base)}${e.sub?`_{${Ze(e.sub)}}`:``}${e.sup?`^{${Ze(e.sup)}}`:``}`;case`math-fraction`:return`\\frac{${Ze(e.numerator)}}{${Ze(e.denominator)}}`;case`math-root`:return`\\sqrt{${Ze(e.radicand)}}`;case`math-row-break`:return`\\\\`;case`math-unsupported`:return e.value;default:return``}}).join(``)}function Qe({source:e}){let t=(0,p.useMemo)(()=>Ae(e),[e]);return(0,m.jsx)(`div`,{className:`md`,"data-md":`1`,children:t.map((e,t)=>tt(e,t))})}function $e({source:e}){let t=(0,p.useMemo)(()=>be(e),[e]);return(0,m.jsx)(`span`,{className:`md md-inline`,children:t.map((e,t)=>rt(e,t))})}function et(e){return ne(e).map((e,t)=>e.kind===`plain`?(0,m.jsx)(`span`,{children:e.text},t):(0,m.jsx)(`span`,{className:`code-token-${e.kind}`,children:e.text},t))}function tt(e,t){switch(e.type){case`paragraph`:return(0,m.jsx)(`p`,{className:`md-p`,children:e.children.map((e,t)=>rt(e,t))},t);case`heading`:{let n=`h${Math.min(6,Math.max(1,e.level))}`;return(0,m.jsx)(n,{className:`md-h md-h${e.level}`,children:e.children.map((e,t)=>rt(e,t))},t)}case`code-block`:return(0,m.jsx)(`pre`,{className:`md-pre`,"data-md-lang":e.lang||void 0,children:(0,m.jsx)(`code`,{className:`md-code`,children:et(e.value)})},t);case`list`:{let n=e.items.map((e,t)=>(0,m.jsx)(`li`,{className:`md-li`,children:e.map((e,t)=>tt(e,t))},t));return e.ordered?(0,m.jsx)(`ol`,{className:`md-ol`,start:e.start>1?e.start:void 0,children:n},t):(0,m.jsx)(`ul`,{className:`md-ul`,children:n},t)}case`blockquote`:return(0,m.jsx)(`blockquote`,{className:`md-quote`,children:e.children.map((e,t)=>tt(e,t))},t);case`rule`:return(0,m.jsx)(`hr`,{className:`md-rule`},t);case`table`:return(0,m.jsx)(`div`,{className:`md-table-wrap`,children:(0,m.jsxs)(`table`,{className:`md-table`,children:[(0,m.jsx)(`thead`,{children:(0,m.jsx)(`tr`,{children:e.head.map((t,n)=>(0,m.jsx)(`th`,{style:nt(e.align[n]),children:t.map((e,t)=>rt(e,t))},n))})}),(0,m.jsx)(`tbody`,{children:e.rows.map((t,n)=>(0,m.jsx)(`tr`,{children:t.map((t,n)=>(0,m.jsx)(`td`,{style:nt(e.align[n]),children:t.map((e,t)=>rt(e,t))},n))},n))})]})},t);case`math-block`:return(0,m.jsx)(it,{source:e.source},t);default:return null}}var nt=e=>e?{textAlign:e}:void 0;function rt(e,t){switch(e.type){case`text`:return(0,m.jsx)(p.Fragment,{children:e.value},t);case`code`:return(0,m.jsx)(`code`,{className:`md-code-inline`,children:e.value},t);case`strong`:return(0,m.jsx)(`strong`,{className:`md-strong`,children:e.children.map((e,t)=>rt(e,t))},t);case`em`:return(0,m.jsx)(`em`,{className:`md-em`,children:e.children.map((e,t)=>rt(e,t))},t);case`del`:return(0,m.jsx)(`del`,{className:`md-del`,children:e.children.map((e,t)=>rt(e,t))},t);case`break`:return(0,m.jsx)(`br`,{className:`md-break`},t);case`math`:return(0,m.jsx)(at,{source:e.source},t);case`link`:{let n=ae(e.href);return n?(0,m.jsx)(`a`,{className:`md-link`,href:n,title:e.title,target:`_blank`,rel:`noopener noreferrer nofollow`,children:e.children.map((e,t)=>rt(e,t))},t):(0,m.jsx)(p.Fragment,{children:e.children.map((e,t)=>rt(e,t))},t)}default:return null}}function it({source:e}){let t=(0,p.useMemo)(()=>Ke(e),[e]);return(0,m.jsx)(`div`,{className:`md-math md-math-block`,"data-md-math":`block`,"data-md-math-source":e,children:t.map((e,t)=>ot(e,t))})}function at({source:e}){let t=(0,p.useMemo)(()=>Ke(e),[e]);return(0,m.jsx)(`span`,{className:`md-math md-math-inline`,"data-md-math":`inline`,"data-md-math-source":e,children:t.map((e,t)=>ot(e,t))})}function ot(e,t){switch(e.type){case`math-text`:return(0,m.jsx)(`span`,{className:`md-m-text`,children:e.value},t);case`math-number`:return(0,m.jsx)(`span`,{className:`md-m-number`,children:e.value},t);case`math-identifier`:return(0,m.jsx)(`i`,{className:`md-m-var`,children:e.value},t);case`math-operator`:return(0,m.jsx)(`span`,{className:`md-m-op`,children:e.value},t);case`math-function`:return(0,m.jsx)(`span`,{className:`md-m-fn`,children:e.value},t);case`math-row-break`:return(0,m.jsx)(`br`,{className:`md-m-rowbreak`},t);case`math-unsupported`:return(0,m.jsx)(`span`,{className:`md-m-unsupported`,"data-md-math-unsupported":`1`,title:e.reason,children:e.value},t);case`math-script`:return(0,m.jsxs)(`span`,{className:`md-m-script`,children:[(0,m.jsx)(`span`,{className:`md-m-base`,children:e.base.map((e,t)=>ot(e,t))}),e.sup&&(0,m.jsx)(`sup`,{className:`md-m-sup`,children:e.sup.map((e,t)=>ot(e,t))}),e.sub&&(0,m.jsx)(`sub`,{className:`md-m-sub`,children:e.sub.map((e,t)=>ot(e,t))})]},t);case`math-fraction`:return(0,m.jsxs)(`span`,{className:`md-m-frac`,children:[(0,m.jsx)(`span`,{className:`md-m-num`,children:e.numerator.map((e,t)=>ot(e,t))}),(0,m.jsx)(`span`,{className:`md-m-den`,children:e.denominator.map((e,t)=>ot(e,t))})]},t);case`math-root`:return(0,m.jsxs)(`span`,{className:`md-m-root`,children:[(0,m.jsx)(`span`,{className:`md-m-radical`,children:`√`}),e.index&&(0,m.jsx)(`sup`,{className:`md-m-index`,children:e.index.map((e,t)=>ot(e,t))}),(0,m.jsx)(`span`,{className:`md-m-body`,children:e.radicand.map((e,t)=>ot(e,t))})]},t);default:return null}}var st=[[`help`,`Show help`],[`model`,`Choose model`],[`settings`,`Open settings`],[`clear`,`Clear session`],[`compact`,`Compact context`],[`reload`,`Reload agents`],[`new`,`New session`],[`fork`,`Fork session`],[`resume`,`Resume session`],[`stop`,`Stop run`],[`skill`,`Workspace skill`]].map(([e,t])=>({id:e,label:`/${e} — ${t}`,description:t})),ct=e=>e.normalize(`NFKD`).replace(/\p{M}/gu,``).toLowerCase();function lt(e,t){let n=ct(t.trim());return e.examples?.find(t=>ct(t).includes(n)&&t!==e.id)??e.id}function ut(e,t){let n=ct(t.trim());return n?e.filter(e=>ct(e.id).includes(n)||ct(e.label).includes(n)||e.examples?.some(e=>ct(e).includes(n))):e}function dt({query:e,anchorRect:t,command:n,workspaceId:r,onSelect:i,onClose:a}){let[o,s]=(0,p.useState)(0),[c,l]=(0,p.useState)(st),u=(0,p.useRef)(null),d=(0,p.useRef)([]);(0,p.useEffect)(()=>{let e=!1;if(r)return fetch(`/api/memory?workspaceId=${encodeURIComponent(r)}`).then(e=>e.ok?e.json():void 0).then(t=>{if(e||!t?.entries?.skills)return;let n=t.entries.skills.map(e=>{let t=e.content.split(/\r?\n/,1)[0].replace(/^#+\s*/,``).trim()||`unnamed-skill`,n=t.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g,`-`).replace(/^-|-$/g,``)||`unnamed-skill`;return{id:`skill:${n}`,label:t,description:`Workspace skill`,examples:[`skill:${n}`]}});l([...st.filter(e=>e.id!==`skill`),...n])}).catch(()=>{}),()=>{e=!0}},[r]);let f=ut([{id:`start`,label:`start — 打开浏览器`,description:`在浏览器中打开 Super Pi 控制台`,action:()=>n({type:`super_pi.start`})},{id:`restart`,label:`restart — 重启 Super Pi 服务`,description:`停掉并重新启动服务,加载磁盘上的新代码`,action:()=>n({type:`super_pi.restart`})},{id:`update`,label:`update — 更新 Super Pi`,description:`运行 npm install -g @evandu/super-pi 然后重启`,action:()=>n({type:`super_pi.update`})},...c],e),h=f.length===0?-1:Math.min(Math.max(o,0),f.length-1),g=e=>s(t=>f.length?Math.min(Math.max(t+e,0),f.length-1):0);(0,p.useEffect)(()=>{let e=e=>{u.current?.contains(e.target)||a()};return window.addEventListener(`mousedown`,e),()=>window.removeEventListener(`mousedown`,e)},[a]),(0,p.useEffect)(()=>{s(0)},[e]),(0,p.useLayoutEffect)(()=>{h>=0&&d.current[h]?.scrollIntoView({block:`nearest`})},[h]);let _={position:`fixed`,left:Math.max(8,Math.min(t.left,window.innerWidth-360)),bottom:Math.max(8,window.innerHeight-t.top+6),zIndex:200};return(0,m.jsx)(`div`,{ref:u,className:`slash-menu`,style:_,role:`listbox`,"aria-label":`斜杠命令`,onKeyDown:e=>{if(e.key===`Escape`)e.preventDefault(),a();else if(e.key===`ArrowDown`)e.preventDefault(),g(1);else if(e.key===`ArrowUp`)e.preventDefault(),g(-1);else if(e.key===`Home`)e.preventDefault(),s(0);else if(e.key===`End`)e.preventDefault(),s(f.length-1);else if(e.key===`Enter`){e.preventDefault();let t=f[h];if(!t)return;t.action?(t.action(),a()):i(t)}},children:f.length===0?(0,m.jsxs)(`div`,{className:`slash-menu-empty`,children:[`没有匹配「`,e,`」的命令`]}):f.map((e,t)=>(0,m.jsxs)(`button`,{ref:e=>{d.current[t]=e},type:`button`,role:`option`,"aria-selected":t===h,className:`slash-menu-item${t===h?` selected`:``}`,"data-slash-command":e.id,onClick:()=>{e.action?(e.action(),a()):i(e)},onMouseEnter:()=>s(t),children:[(0,m.jsxs)(`span`,{className:`slash-menu-item-id`,children:[`/`,e.id]}),(0,m.jsx)(`span`,{className:`slash-menu-item-sep`,"aria-hidden":`true`,children:` — `}),(0,m.jsx)(`span`,{className:`slash-menu-item-label`,children:e.label}),e.examples?.length?(0,m.jsx)(`span`,{className:`slash-menu-item-examples`,children:e.examples.slice(0,2).join(` · `)}):null]},e.id))})}var ft=4e3,pt=` `;function mt(e){return ne(e).map((e,t)=>e.kind===`plain`?(0,m.jsx)(`span`,{children:e.text},t):(0,m.jsx)(`span`,{className:`code-token-${e.kind}`,children:e.text},t))}function ht({value:e,onChange:t,onKeyDown:n,path:r}){return(0,m.jsxs)(`div`,{className:`file-textarea-highlight-wrap`,children:[(0,m.jsx)(`div`,{className:`file-textarea-highlight`,"data-file-text-highlight":r,"aria-hidden":`true`,children:mt(e)}),(0,m.jsx)(`textarea`,{className:`file-textarea`,"data-file-text":r,value:e,wrap:`off`,spellCheck:!1,autoCapitalize:`off`,autoCorrect:`off`,"aria-label":`编辑 ${r}`,onChange:e=>t(e.target.value),onKeyDown:n})]})}var gt=class extends Error{status;body;constructor(e,t){super(t.error||`HTTP ${e}`),this.status=e,this.body=t,this.name=`ApiError`}},_t=15e3,vt=e=>new gt(408,{error:`请求超时:${_t/1e3} 秒内服务端没有响应`,code:`timeout`,details:{path:e}});async function yt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),_t);try{let r=await fetch(e,{...t,signal:n.signal}),i;try{i=await r.json()}catch{i=void 0}if(!r.ok)throw new gt(r.status,i??{error:`HTTP ${r.status}`});return i}catch(t){throw n.signal.aborted?vt(e):t}finally{clearTimeout(r)}}var bt=e=>e instanceof gt?e:new gt(0,{error:e instanceof Error?e.message:`读取失败`}),xt=e=>encodeURIComponent(e),St=(e,t)=>`/api/files?workspaceId=${xt(e)}&path=${xt(t)}`,Ct=(e,t)=>`/api/file?workspaceId=${xt(e)}&path=${xt(t)}`,wt=(e,t)=>`/api/file?workspaceId=${xt(e)}&path=${xt(t)}&meta=1`,Tt=(e,t)=>`/api/files?workspaceId=${xt(e)}&q=${xt(t)}`,Et=e=>e<1024?`${e} B`:e<1048576?`${(e/1024).toFixed(1)} KB`:`${(e/1024/1024).toFixed(1)} MB`,Dt=e=>{let t=new Date(e);return Number.isNaN(t.getTime())?``:new Date(e).toLocaleTimeString(`zh-CN`,{hour:`2-digit`,minute:`2-digit`})};function Ot({workspaceId:e,entryPath:t,name:n,listings:r,expanded:i,toggle:a,loadDirectory:o,openPaths:s,onOpen:c,depth:l}){let u=r[t],d=i.has(t);return(0,p.useEffect)(()=>{d&&u===void 0&&o(t)},[d,t,u,o]),(0,m.jsxs)(`li`,{className:`file-node`,"data-file-dir":t,children:[(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-dir`,"data-file-dir-toggle":t,"aria-expanded":d,style:{paddingLeft:`${8+l*13}px`},onClick:()=>a(t),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`,children:d?`▾`:`▸`}),(0,m.jsx)(`span`,{className:`file-name`,children:n})]}),d&&(0,m.jsx)(`ul`,{className:`file-children`,children:u===void 0?(0,m.jsx)(`li`,{className:`file-row-loading`,"data-file-loading":t,children:`读取中…`}):`error`in u?(0,m.jsx)(`li`,{className:`file-row-error`,"data-file-error":t,children:u.error.message}):u.entries.length===0?(0,m.jsx)(`li`,{className:`file-row-empty`,"data-file-empty":t,children:`空目录`}):(0,m.jsxs)(m.Fragment,{children:[u.truncated&&(0,m.jsx)(`li`,{className:`file-row-empty`,"data-file-truncated":t,children:`条目过多,只显示前一部分`}),u.entries.map(t=>t.kind===`directory`?(0,m.jsx)(Ot,{workspaceId:e,entryPath:t.path,name:t.name,listings:r,expanded:i,toggle:a,loadDirectory:o,openPaths:s,onOpen:c,depth:l+1},t.path):(0,m.jsx)(kt,{entry:t,depth:l+1,openPaths:s,onOpen:c},t.path))]})})]})}function kt({entry:e,depth:t,openPaths:n,onOpen:r}){if(e.kind===`symlink`)return(0,m.jsx)(`li`,{className:`file-node`,"data-file-entry":e.path,"data-file-outside":e.path,children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,disabled:!0,title:`${e.path} 是一个指向 workspace 之外的符号链接,不能打开`,style:{paddingLeft:`${8+t*13}px`},children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),(0,m.jsx)(`span`,{className:`file-badge`,"data-file-outside-badge":`1`,children:`外部链接`})]})});let i=n.has(e.path);return(0,m.jsx)(`li`,{className:`file-node`,"data-file-entry":e.path,"data-file-row-open":i?`1`:`0`,children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,"data-file-open":e.path,disabled:!e.text,title:e.text?i?`${e.path}(已经打开,点击切换到它)`:e.path:`${e.name} 是二进制文件,无法以文本打开`,style:{paddingLeft:`${8+t*13}px`},onClick:()=>r(e),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),i&&(0,m.jsx)(`span`,{className:`file-open-mark`,"data-file-open-mark":e.path,title:`已经打开`,"aria-label":`已经打开`,children:`●`}),!e.text&&(0,m.jsx)(`span`,{className:`file-badge`,"data-file-binary":`1`,"aria-label":`不支持的文件类型`,children:`不支持`})]})})}function At({file:e,state:t,message:n,external:r,onChange:i,onSave:a,onReload:o,onOverwrite:s}){let[c,l]=(0,p.useState)(!0),u=/\.md$/i.test(e.path);return(0,m.jsxs)(`div`,{className:`file-editor`,"data-file-editor":e.path,"data-save-state":t,children:[(0,m.jsxs)(`div`,{className:`file-editor-head`,children:[(0,m.jsx)(`span`,{className:`file-editor-path`,title:e.path,children:e.path}),(0,m.jsxs)(`span`,{className:`file-editor-flags`,children:[u&&(0,m.jsx)(`button`,{type:`button`,className:`file-preview-toggle`,onClick:()=>l(e=>!e),children:c?`原文`:`预览`}),e.dirty&&(0,m.jsx)(`span`,{className:`file-dirty`,"data-file-dirty":`1`,children:`未保存`}),(0,m.jsx)(`button`,{type:`button`,className:`file-save`,"data-file-save":e.path,onClick:a,disabled:t===`saving`,children:t===`saving`?`保存中…`:`保存`})]})]}),r&&t!==`conflict`&&(0,m.jsxs)(`div`,{className:`file-conflict`,"data-file-external":e.path,role:`alert`,children:[(0,m.jsx)(`p`,{children:`这个文件在磁盘上已经被改过了,你的修改还没有覆盖它。`}),(0,m.jsxs)(`div`,{className:`file-conflict-actions`,children:[(0,m.jsx)(`button`,{type:`button`,"data-file-reload":e.path,onClick:o,children:`丢掉我的修改,读磁盘版本`}),(0,m.jsx)(`button`,{type:`button`,"data-file-overwrite":e.path,onClick:s,children:`仍然覆盖磁盘`})]})]}),t===`conflict`&&(0,m.jsxs)(`div`,{className:`file-conflict`,"data-file-conflict":e.path,role:`alert`,children:[(0,m.jsx)(`p`,{children:`保存被拒绝:这个文件在磁盘上已经被改过了,覆盖会丢掉对方的改动。`}),(0,m.jsxs)(`div`,{className:`file-conflict-actions`,children:[(0,m.jsx)(`button`,{type:`button`,"data-file-reload":e.path,onClick:o,children:`丢掉我的修改,读磁盘版本`}),(0,m.jsx)(`button`,{type:`button`,"data-file-overwrite":e.path,onClick:s,children:`仍然覆盖磁盘`})]})]}),n&&t!==`conflict`&&!r&&(0,m.jsx)(`p`,{className:`file-editor-message file-message-${t}`,"data-file-message":t,role:`status`,children:n}),u&&c?(0,m.jsx)(`div`,{className:`file-markdown-preview`,children:(0,m.jsx)(Qe,{source:e.content})}):(0,m.jsx)(ht,{value:e.content,onChange:i,onKeyDown:e=>{let t=e.currentTarget;if(e.nativeEvent.isComposing||e.keyCode===229)return;if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`s`){e.preventDefault(),a();return}if(e.key!==`Tab`)return;e.preventDefault();let{selectionStart:n,selectionEnd:r,value:o}=t;if(!o.slice(n,r).includes(`
21
21
  `)&&!e.shiftKey){i(`${o.slice(0,n)}${pt}${o.slice(r)}`);let e=n+2;requestAnimationFrame(()=>{t.selectionStart=e,t.selectionEnd=e});return}let s=o.lastIndexOf(`
22
22
  `,n-1)+1,c=r+(o[r-1]===`
23
23
  `?0:1),l=o.slice(s,c),u=l.split(`
24
24
  `).map(t=>e.shiftKey?t.replace(RegExp(`^[ \\t]{1,2}`),``):`${pt}${t}`).join(`
25
- `);i(o.slice(0,s)+u+o.slice(c));let d=u.length-l.length;requestAnimationFrame(()=>{let e=Math.max(s,n+Math.min(d,0));t.selectionStart=e,t.selectionEnd=Math.max(e,r+d)})},path:e.path})]})}function jt({error:e}){let t=e.body.code,n=typeof e.body.details?.path==`string`?e.body.details.path:``,r=t===`workspace_missing`?`missing`:t===`timeout`?`timeout`:`failed`;return(0,m.jsxs)(`div`,{className:`rail-empty file-problem`,"data-file-root-error":`1`,"data-file-error-kind":r,role:`alert`,children:[r===`missing`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`工作区路径不存在,无法读取文件。`}),(0,m.jsx)(`p`,{className:`file-problem-hint`,children:`请确认这个路径是写错了,还是目录被移动或删除了:`}),(0,m.jsx)(`code`,{className:`file-problem-path`,"data-file-error-path":`1`,children:n||e.message})]}),r===`timeout`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`读取超时,服务端没有回应。`}),(0,m.jsxs)(`p`,{className:`file-problem-hint`,children:[`已经等了 `,_t/1e3,` 秒。如果这个工作区在网络盘上,请检查它是否已连接,然后点上面的 ↻ 重试。`]})]}),r===`failed`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`读取失败。`}),(0,m.jsx)(`p`,{className:`file-problem-hint`,children:e.message})]})]})}function Mt(e){let[t,n]=(0,p.useState)([]),[r,i]=(0,p.useState)(),[a,o]=(0,p.useState)(``),[s,c]=(0,p.useState)({}),[l,u]=(0,p.useState)({});(0,p.useEffect)(()=>{n([]),i(void 0),o(``),c({}),u({})},[e]);let d=(0,p.useCallback)(async r=>{if(e){if(r.text===!1)o(`「${r.name}」不支持以文本打开(二进制文件)。`);else if(t.some(e=>e.path===r.path))i(r.path);else{o(``);try{let t=await yt(Ct(e,r.path));n(e=>e.some(e=>e.path===r.path)?e:[...e,{path:t.path,name:r.name,content:t.content,etag:t.meta.etag,dirty:!1}]),i(t.path),u(e=>({...e,[t.path]:!1}))}catch(e){o(`打不开「${r.name}」:${e instanceof Error?e.message:`未知错误`}`)}}}},[e,t]),f=(0,p.useCallback)(e=>{n(t=>{let n=t.find(t=>t.path===e);if(n?.dirty&&!window.confirm(`「${n.name}」有未保存的修改,关掉就丢了。仍然关闭?`))return t;let a=t.findIndex(t=>t.path===e),o=t.filter(t=>t.path!==e);return e===r&&i(o.length===0?void 0:o[Math.min(a,o.length-1)]?.path),o})},[r]),h=(0,p.useCallback)((e,t)=>{n(n=>n.map(n=>n.path===e?{...n,content:t,dirty:!0}:n))},[]);(0,p.useEffect)(()=>{if(!e||t.length===0)return;let n=!0,r=t.map(e=>e.path),i=setInterval(()=>{for(let i of r)yt(wt(e,i)).then(e=>{if(!n)return;let r=t.find(e=>e.path===i);r&&e.etag!==r.etag&&u(e=>({...e,[i]:!0}))}).catch(()=>{})},ft);return()=>{n=!1,clearInterval(i)}},[e,t]);let g=(0,p.useCallback)(async(r,i)=>{if(!e)return;let a=t.find(e=>e.path===r);if(!a)return;c(e=>({...e,[r]:{state:`saving`,message:``}}));let o=a.etag;if(i)try{o=(await yt(wt(e,r))).etag}catch(e){c(t=>({...t,[r]:{state:`error`,message:`无法读取磁盘上的最新版本:${e instanceof Error?e.message:`未知错误`}`}}));return}try{let t=await yt(Ct(e,r),{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({path:r,content:a.content,baseEtag:o})});n(e=>e.map(e=>e.path===r?{...e,etag:t.meta.etag,dirty:!1}:e)),u(e=>({...e,[r]:!1})),c(e=>({...e,[r]:{state:`saved`,message:`已保存 ${Dt(t.meta.modifiedAt)}`}}))}catch(e){let t=e instanceof gt&&e.body.code===`stale`;c(n=>({...n,[r]:{state:t?`conflict`:`error`,message:e instanceof Error?e.message:`保存失败`}})),t&&u(e=>({...e,[r]:!0}))}},[e,t]),_=(0,p.useCallback)(async t=>{if(e)try{let r=await yt(Ct(e,t));n(e=>e.map(e=>e.path===t?{...e,content:r.content,etag:r.meta.etag,dirty:!1}:e)),u(e=>({...e,[t]:!1})),c(e=>({...e,[t]:{state:`saved`,message:`已重新载入磁盘版本`}}))}catch(e){c(n=>({...n,[t]:{state:`error`,message:e instanceof Error?e.message:`重新载入失败`}}))}},[e]);return{open:t,activePath:r,active:t.find(e=>e.path===r),tabItems:(0,p.useMemo)(()=>t.map(e=>({id:e.path,label:e.name,title:e.path,closable:!0,badge:e.dirty?(0,m.jsx)(`span`,{className:`file-tab-badge`,"data-file-tab-dirty":e.path,"aria-label":`未保存`,children:`●`}):void 0})),[t]),openPaths:(0,p.useMemo)(()=>new Set(t.map(e=>e.path)),[t]),notice:a,setNotice:o,saves:s,external:l,openFile:d,setActivePath:i,closeFile:f,updateBuffer:h,save:g,reload:_}}function Nt({workspaceId:e,files:t,onOpenFile:n}){let{open:r,openPaths:i,notice:a,setNotice:o,openFile:s}=t,[c,l]=(0,p.useState)({}),[u,d]=(0,p.useState)(()=>new Set),[f,h]=(0,p.useState)(``),[g,_]=(0,p.useState)(null),[v,y]=(0,p.useState)(!1),b=(0,p.useCallback)(e=>{n?n(e):s(e)},[n,s]),x=(0,p.useCallback)(e=>{d(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),S=(0,p.useCallback)(e=>{l({}),l(e=>({...e,"":void 0})),yt(St(e,``)).then(e=>l({"":e})).catch(e=>l({"":{error:bt(e)}})),d(new Set)},[]);(0,p.useEffect)(()=>{if(l({}),_(null),o(``),!e)return;let t=!0;return yt(St(e,``)).then(e=>{t&&l({"":e})}).catch(e=>{t&&l({"":{error:bt(e)}})}),()=>{t=!1}},[e]),(0,p.useEffect)(()=>{let t=f.trim();if(!e||!t){_(null),y(!1);return}let n=!0;y(!0);let r=setTimeout(()=>{yt(Tt(e,t)).then(e=>{n&&(_(e),y(!1))}).catch(e=>{n&&(_({mode:`search`,query:t,hits:[],truncated:!1,skipped:[]}),y(!1),o(`搜索失败:${e instanceof Error?e.message:`未知错误`}`))})},220);return()=>{n=!1,clearTimeout(r)}},[f,e]);let C=c[``],w=f.trim(),T=(0,p.useMemo)(()=>w?v||!g?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-searching":`1`,children:`搜索中…`}):g.hits.length===0?(0,m.jsxs)(`p`,{className:`rail-empty`,"data-file-search-empty":`1`,children:[`没有匹配「`,w,`」的文件。`]}):(0,m.jsxs)(m.Fragment,{children:[g.truncated&&(0,m.jsx)(`p`,{className:`file-search-note`,"data-file-search-partial":`1`,children:`结果不完整:搜索有上限,只列出了一部分。`}),g.skipped.length>0&&(0,m.jsxs)(`p`,{className:`file-search-note`,"data-file-search-skipped":`1`,children:[`已跳过 `,g.skipped.length,` 个目录(node_modules、.git 等)。`]}),(0,m.jsx)(`ul`,{className:`file-list`,children:g.hits.map(e=>(0,m.jsx)(`li`,{children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,"data-file-hit":e.path,"data-file-hit-open":i.has(e.path)?`1`:`0`,onClick:()=>b({path:e.path,name:e.name,text:!0}),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),i.has(e.path)&&(0,m.jsx)(`span`,{className:`file-open-mark`,"data-file-open-mark":e.path,title:`已经打开`,"aria-label":`已经打开`,children:`●`}),(0,m.jsx)(`span`,{className:`file-badge-path`,children:e.directory||`/`})]})},e.path))})]}):e?C===void 0?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-root-loading":`1`,children:`读取工作区…`}):`error`in C?(0,m.jsx)(jt,{error:C.error}):(0,m.jsxs)(m.Fragment,{children:[C.truncated&&(0,m.jsx)(`p`,{className:`file-search-note`,"data-file-truncated-root":`1`,children:`条目过多,只显示前一部分。`}),C.entries.length===0?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-root-empty":`1`,children:`这个工作区是空的,没有文件也没有子目录。`}):(0,m.jsx)(`ul`,{className:`file-list`,children:C.entries.map(t=>t.kind===`directory`?(0,m.jsx)(Ot,{workspaceId:e,entryPath:t.path,name:t.name,listings:c,expanded:u,toggle:x,openPaths:i,onOpen:e=>b(e),onError:o,depth:0},t.path):(0,m.jsx)(kt,{entry:t,depth:0,openPaths:i,onOpen:e=>b(e)},t.path))})]}):(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-no-workspace":`1`,children:`先在左侧选一个工作区,这里会显示它的文件。`}),[w,v,g,e,C,c,u,x,b,i]);return(0,m.jsxs)(`div`,{className:`files-panel`,"data-files-panel":`1`,"data-files-open-count":r.length,children:[(0,m.jsxs)(`div`,{className:`files-search`,children:[(0,m.jsx)(`input`,{type:`search`,className:`files-search-input`,"data-file-search-input":`1`,value:f,placeholder:`按文件名搜索…`,"aria-label":`按文件名搜索`,onChange:e=>h(e.target.value)}),(0,m.jsx)(`button`,{type:`button`,className:`files-refresh`,"data-file-refresh":`1`,title:`重新读取目录`,"aria-label":`重新读取目录`,disabled:!e,onClick:()=>e&&S(e),children:`↻`})]}),a&&(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-notice":`1`,children:a}),(0,m.jsx)(`div`,{className:`files-tree`,"data-file-tree":w?`search`:`browse`,children:T}),r.length===0&&(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-none-open":`1`,children:`点一个文本文件,它会在上面的标签栏开一个标签,正文显示在中间主画面。`}),r.length>0&&(0,m.jsxs)(`p`,{className:`files-hint`,"data-file-open-hint":`1`,children:[`已打开 `,r.length,` 个文件,正文显示在中间主画面;点上面的标签切换,点这里的行切换。`]}),(0,m.jsxs)(`p`,{className:`files-footnote`,"data-files-footnote":`1`,children:[`只支持纯文本(md / 代码),二进制文件不会打开。写入只允许覆盖已存在的文件,不新建,不删除。`,r.length>0&&` 当前打开 ${r.length} 个文件,占 ${Et(r.reduce((e,t)=>e+t.content.length,0))}。`]})]})}function Pt(e,t,n=88,r=200){let i=Math.max(0,Math.floor(e));if(!Number.isFinite(t)||t<=0)return{perRow:Math.max(1,i),width:n};if(i===0)return{perRow:1,width:n};if(i===1)return{perRow:1,width:Math.min(r,t)};let a=t/i;if(a>=n)return{perRow:i,width:Math.min(a,r)};for(let e=2;e<=i;e+=1){let a=Math.ceil(i/e),o=t/a;if(o>=n)return{perRow:a,width:Math.min(o,r)}}return{perRow:1,width:n}}function Ft(e,t){let n=Math.max(1,Math.floor(t)),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return r}function It(e,t,n=88,r=200){let[i,a]=(0,p.useState)(()=>Pt(e,0,n,r)),[o,s]=(0,p.useState)(null),c=(0,p.useCallback)(()=>{let i=Pt(e,t,n,r);s({count:e,containerWidth:t}),a(e=>e.perRow===i.perRow&&e.width===i.width?e:i)},[e,t,n,r]),l=(0,p.useRef)(t);(0,p.useEffect)(()=>{t<=0||l.current!==t&&(l.current=t,c())},[t,c]);let u=(0,p.useRef)(null);return(0,p.useEffect)(()=>{let t=u.current;u.current=e,t!==null&&e>t&&c()},[e,c]),{layout:i,frozen:o!==null&&o.count!==e,recompute:c}}function Lt({tabs:e,activeId:t,onSelect:n,onClose:r,ariaLabel:i,minWidth:a=88,maxWidth:o=200,prefix:s=`workspace-tab`,idPrefix:c=s,className:l}){let u=(0,p.useRef)(null),[d,f]=(0,p.useState)(0),{layout:h,frozen:g,recompute:_}=It(e.length,d,a,o);(0,p.useLayoutEffect)(()=>{let e=u.current;if(!e||typeof ResizeObserver>`u`)return;let t=()=>{let t=e.clientWidth;t>0&&f(t)};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let v=Ft(e,h.perRow),y=(e,t)=>t===void 0?{}:{[`data-${s}-${e}`]:t},b=e.filter(e=>e.kind===`file`).length,x=e.filter(e=>e.kind===`session`||!e.kind).length,S=t=>({[`data-${t}-strip`]:`1`,...g?{[`data-${t}-frozen`]:`1`}:{},[`data-${t}-count`]:t===`file-tab`?b:t===`session-tab`?x:e.length,[`data-${t}-min`]:a,[`data-${t}-max`]:o,[`data-${t}-per-row`]:h.perRow,[`data-${t}-width`]:Math.round(h.width*100)/100}),C=e=>{let t=e.relatedTarget;(!t||t.isConnected)&&_()},w=e=>{let t=e.relatedTarget;(!t||t.isConnected)&&_()};(0,p.useEffect)(()=>{if(!g)return;let e=u.current;if(!e)return;let t=t=>{let n=e.getBoundingClientRect();t.clientX>=n.left&&t.clientX<=n.right&&t.clientY>=n.top&&t.clientY<=n.bottom||_()};return document.addEventListener(`mousemove`,t,!0),()=>document.removeEventListener(`mousemove`,t,!0)},[g,_]);let T=[`${s}-strip`,`workspace-tab-strip`,`session-tab-strip`,`file-tab-strip`,l].filter(Boolean).join(` `);return(0,m.jsx)(`div`,{ref:u,className:T,role:`tablist`,"aria-label":i??`工作区标签页`,...y(`strip`,`1`),...S(`workspace-tab`),...S(`session-tab`),...S(`file-tab`),style:{"--tab-w":`${h.width}px`},onMouseEnter:C,onMouseLeave:w,children:v.map((i,a)=>(0,m.jsx)(`div`,{className:`${s}-row workspace-tab-row session-tab-row file-tab-row`,role:`presentation`,...y(`row`,a),children:i.map(i=>{let a=i.id===t||i.rawId!==void 0&&i.rawId===t,o=i.title??i.label,l=i.kind,u=i.rawId??i.id,d=e.findIndex(e=>e.id===i.id),f=[s,a?`${s}-active`:``,`workspace-tab`,a?`workspace-tab-active`:``,l?`${l}-tab`:``,l&&a?`${l}-tab-active`:``].filter(Boolean).join(` `);return(0,m.jsxs)(`div`,{role:`tab`,id:`${c}-${i.id}`,"aria-selected":a,"aria-controls":`${c}-panel-${i.id}`,tabIndex:a?0:-1,className:f,...y(`id`,u),...y(`index`,d),...a?y(`active`,`1`):{},...y(`closable`,i.closable===!1?`0`:`1`),...l?{"data-tab-kind":l}:{},...l===`session`||!l?{"data-session-tab-id":u,"data-session-tab-index":d,...a?{"data-session-tab-active":`1`}:{},"data-session-tab-closable":i.closable===!1?`0`:`1`}:{},...l===`file`?{"data-file-tab-id":u,"data-file-tab-index":d,...a?{"data-file-tab-active":`1`}:{},"data-file-tab-closable":i.closable===!1?`0`:`1`}:{},title:o,style:{width:h.width,boxSizing:`border-box`},onClick:()=>n?.(i.id),onAuxClick:e=>{e.button===1&&(e.preventDefault(),e.stopPropagation(),i.closable!==!1&&r&&r(i.id))},onMouseDown:e=>{e.button===1&&e.preventDefault()},onKeyDown:t=>{if(t.key!==`ArrowLeft`&&t.key!==`ArrowRight`)return;t.preventDefault();let r=t.key===`ArrowRight`?1:-1,a=e[(e.findIndex(e=>e.id===i.id)+r+e.length)%e.length];a&&n?.(a.id)},children:[i.icon&&(0,m.jsx)(`span`,{className:`${s}-icon workspace-tab-icon`,children:i.icon}),i.badge,(0,m.jsx)(`span`,{className:`${s}-label workspace-tab-label ${l?`${l}-tab-label`:``}`,...y(`label`,u),...l===`session`||!l?{"data-session-tab-label":u}:{},...l===`file`?{"data-file-tab-label":u}:{},children:i.label}),i.closable!==!1&&r&&(0,m.jsx)(`button`,{type:`button`,className:`${s}-close workspace-tab-close ${l?`${l}-tab-close`:``}`,...y(`close`,u),...l===`session`||!l?{"data-session-tab-close":u}:{},...l===`file`?{"data-file-tab-close":u}:{},onClick:e=>{e.stopPropagation(),r(i.id)},title:`关闭 ${o}`,"aria-label":`关闭 ${o}`,children:`×`})]},i.id)})},`row_${a}`))})}var Rt={pending:`未开始`,running:`运行中`,completed:`已完成`,failed:`失败`,skipped:`已跳过`,stopped:`已停止`},zt={running:`运行中`,completed:`全部完成`,failed:`中途失败`,stopped:`已停止`},Bt={pending:`○`,running:`◐`,completed:`✓`,failed:`✕`,skipped:`→`,stopped:`■`},Vt={stop:`失败即停`,skip:`失败跳过`,"retry:1":`失败重试 1 次`,"retry:2":`失败重试 2 次`,"retry:3":`失败重试 3 次`};function Ht(e){let t=Math.max(0,Math.round(e/1e3)),n=t%60,r=Math.floor(t/60)%60,i=Math.floor(t/3600),a=e=>String(e).padStart(2,`0`);return i>0?`${i}h ${a(r)}:${a(n)}`:`${r}:${a(n)}`}function Ut(e){let t=e.status===`running`,[n,r]=(0,p.useState)(0);(0,p.useEffect)(()=>{if(!t)return;r(Date.now());let e=window.setInterval(()=>r(Date.now()),200);return()=>window.clearInterval(e)},[t,e.startedAt]);let i=Date.parse(e.startedAt);if(Number.isFinite(i)){if(!t){let t=Date.parse(e.finishedAt??``);return Number.isFinite(t)?t-i:void 0}return n>0?n-i:void 0}}function Wt({step:e,names:t}){let n=e.attempts.length,r=e.attempts[n-1],i=t.get(e.agentId)||e.agentId,a=e.label?.trim()||i;return(0,m.jsxs)(`li`,{className:`workflow-step workflow-step-${e.status}`,"data-workflow-step":e.index+1,"data-workflow-step-status":e.status,"data-workflow-step-agent":e.agentId,"data-workflow-step-attempts":n,children:[(0,m.jsx)(`span`,{className:`workflow-step-mark`,"aria-hidden":`true`,children:Bt[e.status]}),(0,m.jsxs)(`span`,{className:`workflow-step-n`,"data-workflow-step-n":e.index+1,children:[`第 `,e.index+1,` 步`]}),(0,m.jsx)(`strong`,{className:`workflow-step-name`,"data-workflow-step-name":e.index+1,children:a}),(0,m.jsx)(`span`,{className:`workflow-step-agent`,"data-workflow-step-agent-name":e.index+1,children:i}),n>1&&(0,m.jsxs)(`span`,{className:`workflow-step-tries`,"data-workflow-step-retried":e.index+1,children:[`尝试 `,n,` 次`]}),(0,m.jsx)(`span`,{className:`workflow-step-status`,"data-workflow-step-status-text":e.index+1,children:Rt[e.status]}),e.status===`failed`&&r?.error&&(0,m.jsx)(`span`,{className:`workflow-step-why`,"data-workflow-step-error":e.index+1,children:r.error})]})}function Gt({workflow:e,names:t,onStop:n,stopping:r}){let i=e.status===`running`,a=Ut(e),o=e.steps.filter(e=>e.status===`completed`||e.status===`skipped`).length;return(0,m.jsxs)(`article`,{className:`workflow-card workflow-${e.status}`,"data-workflow-card":e.id,"data-workflow-status":e.status,"data-workflow-steps":e.steps.length,"data-workflow-current":e.currentIndex+1,children:[(0,m.jsxs)(`div`,{className:`workflow-head`,children:[(0,m.jsx)(`span`,{className:`workflow-badge`,children:`编排`}),(0,m.jsx)(`strong`,{className:`workflow-summary`,"data-workflow-summary":e.id,children:e.summary}),(0,m.jsxs)(`span`,{className:`workflow-progress`,"data-workflow-progress":e.id,children:[o,`/`,e.steps.length]}),(0,m.jsx)(`span`,{className:`workflow-status`,"data-workflow-status-text":e.id,children:zt[e.status]??e.status}),(0,m.jsx)(`span`,{className:`workflow-policy`,"data-workflow-policy":e.onFailure,children:Vt[e.onFailure]??e.onFailure}),a!==void 0&&(0,m.jsxs)(`span`,{className:`workflow-clock`,"data-workflow-elapsed":e.id,children:[i?`已运行 `:`用时 `,Ht(a)]}),i&&(0,m.jsx)(`button`,{type:`button`,className:`workflow-stop`,"data-workflow-stop":e.id,disabled:r,onClick:n,children:`停止`})]}),(0,m.jsx)(`ol`,{className:`workflow-steps`,"data-workflow-step-count":e.steps.length,children:e.steps.map(e=>(0,m.jsx)(Wt,{step:e,names:t},e.index))})]})}var j=[{id:`agents`,label:`智能体与创建`,icon:`🤖`,title:`Agent 智能体系统与创建指南`,description:`了解 Super Pi 的角色设定机制,以及如何一步步创建你自己的自定义 Agent。`},{id:`workspace`,label:`工作区与会话`,icon:`🗂️`,title:`工作区与多会话管理`,description:`高效管理本地代码仓库、独立会话标签页与多模态图文输入。`},{id:`subagents`,label:`子任务与编排`,icon:`⚡`,title:`子智能体委托与顺序编排使用指南`,description:`让主 Agent 安排分工、交接结果,并查看进度、处理失败与停止任务。`},{id:`model-policy`,label:`模型使用规则`,icon:`🔒`,title:`模型过滤规则(按工作区限制可用模型)`,description:`用正则规则限定哪些工作区只能用哪些模型,防止敏感代码被发到不该用的模型上。`},{id:`memory`,label:`主动记忆`,icon:`🧠`,title:`主动记忆与技能经验`,description:`让 Super Pi 在得到明确证据后保存长期事实、用户偏好和可复用工作方法。`},{id:`knowledge`,label:`知识库`,icon:`📚`,title:`工作区知识库`,description:`导入规范与设计文档,让 Agent 检索原文作答,而不是凭记忆编造。`},{id:`tools`,label:`侧边栏工具`,icon:`🧩`,title:`全局工具:开发、导出与导入`,description:`让 Agent 写一个工具,用一个文件分享给别人,导入前先看清它会做什么。`},{id:`cli`,label:`指令与快捷键`,icon:`⌨️`,title:`Pi 终端命令与键盘快捷操作`,description:`利用 Pi 终端斜杠指令控制服务生命周期,掌握常用快捷键提升效率。`},{id:`themes`,label:`外观与主题`,icon:`🎨`,title:`界面主题与个性化显示`,description:`内置 4 款专业高对比度主题,针对不同环境提供舒适体验。`}];function Kt({onClose:e}){let[t,n]=(0,p.useState)(`agents`);(0,p.useEffect)(()=>{let t=t=>{t.key===`Escape`&&!t.defaultPrevented&&e()};return window.addEventListener(`keydown`,t),()=>window.removeEventListener(`keydown`,t)},[e]);let r=j.find(e=>e.id===t)??j[0];return(0,m.jsx)(`div`,{className:`overlay`,onClick:e,children:(0,m.jsxs)(`div`,{className:`picker settings-window help-window`,"data-help-window":`1`,onClick:e=>e.stopPropagation(),role:`dialog`,"aria-modal":`true`,"aria-labelledby":`help-window-title`,children:[(0,m.jsxs)(`div`,{className:`settings-window-head help-window-head`,children:[(0,m.jsxs)(`div`,{className:`help-head-title`,children:[(0,m.jsx)(`h2`,{id:`help-window-title`,children:`使用指南与帮助`}),(0,m.jsx)(`span`,{className:`help-badge`,children:`v0.1.x`})]}),(0,m.jsx)(`button`,{type:`button`,className:`help-close-button`,"aria-label":`关闭指南`,onClick:e,children:`✕`})]}),(0,m.jsxs)(`div`,{className:`settings-window-body help-window-body`,children:[(0,m.jsxs)(`nav`,{className:`settings-nav help-nav`,role:`tablist`,"aria-orientation":`vertical`,"aria-label":`使用指南分类`,children:[(0,m.jsx)(`div`,{className:`settings-nav-label`,children:`目录`}),j.map(e=>(0,m.jsxs)(`button`,{type:`button`,id:`help-tab-${e.id}`,className:`settings-category help-category`,role:`tab`,"aria-selected":e.id===r.id,"aria-controls":`help-panel`,tabIndex:e.id===r.id?0:-1,onClick:()=>n(e.id),children:[(0,m.jsx)(`span`,{className:`help-tab-icon`,"aria-hidden":`true`,children:e.icon}),(0,m.jsx)(`span`,{children:e.label})]},e.id))]}),(0,m.jsxs)(`div`,{id:`help-panel`,className:`settings-panel help-panel`,role:`tabpanel`,"aria-labelledby":`help-tab-${r.id}`,children:[(0,m.jsxs)(`div`,{className:`help-panel-header`,children:[(0,m.jsx)(`h3`,{className:`help-panel-title`,children:r.title}),(0,m.jsx)(`p`,{className:`help-panel-desc`,children:r.description})]}),(0,m.jsxs)(`div`,{className:`help-panel-content`,children:[r.id===`agents`&&(0,m.jsx)(Yt,{}),r.id===`workspace`&&(0,m.jsx)(Xt,{}),r.id===`subagents`&&(0,m.jsx)(Qt,{}),r.id===`model-policy`&&(0,m.jsx)(Zt,{}),r.id===`memory`&&(0,m.jsx)(qt,{}),r.id===`knowledge`&&(0,m.jsx)(Jt,{}),r.id===`tools`&&(0,m.jsx)(M,{}),r.id===`cli`&&(0,m.jsx)($t,{}),r.id===`themes`&&(0,m.jsx)(en,{})]}),(0,m.jsx)(`div`,{className:`picker-actions help-actions`,children:(0,m.jsx)(`button`,{type:`button`,className:`picker-cancel`,onClick:e,children:`关闭`})})]})]})]})})}function qt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`🧠 它会记住什么?`}),(0,m.jsx)(`p`,{children:`用户档案保存跨工作区的长期偏好;工作区事实保存环境与项目约定;技能保存已经验证过的步骤、命令和坑。临时结果、秘密和未验证的失败不会保存。`})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`主动保存与后台复盘`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[`主 Agent 可以使用 `,(0,m.jsx)(`code`,{children:`memory`}),` 和 `,(0,m.jsx)(`code`,{children:`skill_manage`}),` 工具主动保存;说“记住了”本身不会写入。`]}),(0,m.jsx)(`li`,{children:`每隔若干回合,系统会在独立会话中复盘最近证据。复盘不能读写工作区文件、执行命令或派发任务。`}),(0,m.jsx)(`li`,{children:`读取记忆和历史只返回带来源的参考数据,不能把记忆中的文字当作新的指令。`})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`权限与审批`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`子 Agent 可以读取相关记忆,但没有写入权限。`}),(0,m.jsx)(`li`,{children:`后台复盘新增记忆可以直接保存;修改或删除旧条目会进入设置中的待审批列表。`}),(0,m.jsx)(`li`,{children:`在“设置 → 主动记忆”可以开关功能、调整复盘间隔、编辑条目和批准提案。`})]})]})]})}function Jt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`📚 它解决什么问题?`}),(0,m.jsx)(`p`,{children:`Agent 默认靠训练时的记忆回答项目里的问题。知识库让你把规范、设计稿、会议纪要导入工作区,Agent 在回答前检索这些文档的原文片段,而不是凭印象编造。它与“主动记忆”不同:记忆是 Agent 自己沉淀的事实和技能,知识库只有你能写入。`})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`怎么用`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`在“设置 → 知识库”里点“浏览工作区文件…”,在弹出的浏览器里挑一个 Markdown 或文本文件,挑完即导入。也可以直接粘贴正文,或手动填写相对路径。`}),(0,m.jsx)(`li`,{children:`同一个文件内容改了再导入就是更新,不会留下旧版本;同样的内容换个标题再导入也不会变成两篇。`}),(0,m.jsx)(`li`,{children:`面板里的“试检索”会用和 Agent 相同的检索器回答,方便你在交给 Agent 之前先确认它找得到。`}),(0,m.jsxs)(`li`,{children:[`从下一条消息开始生效。Agent 使用 `,(0,m.jsx)(`code`,{children:`knowledge_search`}),` 检索,只拿得到片段和来源,拿不到整篇文档。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`语义检索是内置的`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[`关键词检索始终可用。语义检索让“撤销购买”也能找到写着“取消订单”的段落,它靠一个`,(0,m.jsx)(`strong`,{children:`随安装包一起分发`}),`的中文模型在本机运行,不需要额外下载、不需要联网、也不会把文档发到任何服务。`]}),(0,m.jsxs)(`li`,{children:[`第一次检索时加载模型(约一秒),之后一直可用。设 `,(0,m.jsx)(`code`,{children:`SUPER_PI_KNOWLEDGE_EMBEDDINGS=0`}),` 可以彻底关闭语义检索,只保留关键词检索。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`它不知道的事`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`知识库只会检索它有的东西。文档里没写的,它检索出来也不会有——所以导入什么,直接决定它能回答什么。`}),(0,m.jsx)(`li`,{children:`语义检索总会返回“最接近的那一段”,即使这一段其实并不回答问题。检索结果带一个分数,低分意味着这可能只是最像的一条,Agent 会被要求先核对再使用。`}),(0,m.jsx)(`li`,{children:`Agent 对知识库只读,不能写入或删除;导入和删除只有你能做。`})]})]})]})}function Yt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`💡 Agent 是什么?`}),(0,m.jsxs)(`p`,{children:[`在 Super Pi 中,`,(0,m.jsx)(`strong`,{children:`Agent(智能体)`}),` 是一套独立的「角色提示词设定与能力配置」。 通过切换 Agent,模型将扮演专精工程师、系统架构师、代码审查员或规划专家等不同角色,遵循指定的原则来协助你完成任务。`]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`📁 Agent 文件存放在哪里?`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`内置 Agent 目录:`}),`项目根目录下的 `,(0,m.jsx)(`code`,{children:`agents/`}),` 文件夹,例如 `,(0,m.jsx)(`code`,{children:`agents/coder.md`}),`、`,(0,m.jsx)(`code`,{children:`agents/planner.md`}),`、`,(0,m.jsx)(`code`,{children:`agents/reviewer.md`}),` 等。`]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`用户自定义持久化目录:`}),`用户主目录下的 `,(0,m.jsx)(`code`,{children:`~/.super-pi/agents/`}),`,放入此目录的文件在所有项目和更新中均持久有效。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`🚀 如何创建一个全新的自定义 Agent?`}),(0,m.jsxs)(`div`,{className:`help-steps`,children:[(0,m.jsxs)(`div`,{className:`help-step`,children:[(0,m.jsx)(`span`,{className:`step-num`,children:`1`}),(0,m.jsxs)(`div`,{className:`step-body`,children:[(0,m.jsx)(`strong`,{children:`创建 Markdown 文件`}),(0,m.jsxs)(`p`,{children:[`在 `,(0,m.jsx)(`code`,{children:`agents/`}),` 或 `,(0,m.jsx)(`code`,{children:`~/.super-pi/agents/`}),` 目录下新建文件,例如 `,(0,m.jsx)(`code`,{children:`architect.md`}),`。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-step`,children:[(0,m.jsx)(`span`,{className:`step-num`,children:`2`}),(0,m.jsxs)(`div`,{className:`step-body`,children:[(0,m.jsx)(`strong`,{children:`编写元数据与系统提示词`}),(0,m.jsx)(`p`,{children:`文件开头使用 YAML Frontmatter 声明名称和描述,正文编写设定提示词:`}),(0,m.jsx)(`pre`,{className:`help-code-block`,children:(0,m.jsx)(`code`,{children:`---
25
+ `);i(o.slice(0,s)+u+o.slice(c));let d=u.length-l.length;requestAnimationFrame(()=>{let e=Math.max(s,n+Math.min(d,0));t.selectionStart=e,t.selectionEnd=Math.max(e,r+d)})},path:e.path})]})}function jt({error:e}){let t=e.body.code,n=typeof e.body.details?.path==`string`?e.body.details.path:``,r=t===`workspace_missing`?`missing`:t===`timeout`?`timeout`:`failed`;return(0,m.jsxs)(`div`,{className:`rail-empty file-problem`,"data-file-root-error":`1`,"data-file-error-kind":r,role:`alert`,children:[r===`missing`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`工作区路径不存在,无法读取文件。`}),(0,m.jsx)(`p`,{className:`file-problem-hint`,children:`请确认这个路径是写错了,还是目录被移动或删除了:`}),(0,m.jsx)(`code`,{className:`file-problem-path`,"data-file-error-path":`1`,children:n||e.message})]}),r===`timeout`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`读取超时,服务端没有回应。`}),(0,m.jsxs)(`p`,{className:`file-problem-hint`,children:[`已经等了 `,_t/1e3,` 秒。如果这个工作区在网络盘上,请检查它是否已连接,然后点上面的 ↻ 重试。`]})]}),r===`failed`&&(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`p`,{className:`file-problem-title`,"data-file-error-title":`1`,children:`读取失败。`}),(0,m.jsx)(`p`,{className:`file-problem-hint`,children:e.message})]})]})}function Mt(e){let[t,n]=(0,p.useState)([]),[r,i]=(0,p.useState)(),[a,o]=(0,p.useState)(``),[s,c]=(0,p.useState)({}),[l,u]=(0,p.useState)({}),[d,f]=(0,p.useState)({}),[h,g]=(0,p.useState)(()=>new Set),[_,v]=(0,p.useState)(``),[y,b]=(0,p.useState)(null),[x,S]=(0,p.useState)(!1);(0,p.useEffect)(()=>{if(n([]),i(void 0),o(``),c({}),u({}),f({}),g(new Set),b(null),v(``),S(!1),!e)return;let t=!0;return yt(St(e,``)).then(e=>{t&&f({"":e})}).catch(e=>{t&&f({"":{error:bt(e)}})}),()=>{t=!1}},[e]);let C=(0,p.useCallback)(t=>{e&&yt(St(e,t)).then(e=>{f(n=>({...n,[t]:e}))}).catch(e=>{f(n=>({...n,[t]:{error:bt(e)}}))})},[e]),w=(0,p.useCallback)(e=>{g(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),T=(0,p.useCallback)(()=>{e&&(f({}),g(new Set),yt(St(e,``)).then(e=>{f({"":e})}).catch(e=>{f({"":{error:bt(e)}})}))},[e]);(0,p.useEffect)(()=>{let t=_.trim();if(!e||!t){b(null),S(!1);return}let n=!0;S(!0);let r=setTimeout(()=>{yt(Tt(e,t)).then(e=>{n&&(b(e),S(!1))}).catch(e=>{n&&(b({mode:`search`,query:t,hits:[],truncated:!1,skipped:[]}),S(!1),o(`搜索失败:${e instanceof Error?e.message:`未知错误`}`))})},220);return()=>{n=!1,clearTimeout(r)}},[_,e]);let ee=(0,p.useCallback)(async r=>{if(e){if(r.text===!1)o(`「${r.name}」不支持以文本打开(二进制文件)。`);else if(t.some(e=>e.path===r.path))i(r.path);else{o(``);try{let t=await yt(Ct(e,r.path));n(e=>e.some(e=>e.path===r.path)?e:[...e,{path:t.path,name:r.name,content:t.content,etag:t.meta.etag,dirty:!1}]),i(t.path),u(e=>({...e,[t.path]:!1}))}catch(e){o(`打不开「${r.name}」:${e instanceof Error?e.message:`未知错误`}`)}}}},[e,t]),E=(0,p.useCallback)(e=>{n(t=>{let n=t.find(t=>t.path===e);if(n?.dirty&&!window.confirm(`「${n.name}」有未保存的修改,关掉就丢了。仍然关闭?`))return t;let a=t.findIndex(t=>t.path===e),o=t.filter(t=>t.path!==e);return e===r&&i(o.length===0?void 0:o[Math.min(a,o.length-1)]?.path),o})},[r]),te=(0,p.useCallback)((e,t)=>{n(n=>n.map(n=>n.path===e?{...n,content:t,dirty:!0}:n))},[]);(0,p.useEffect)(()=>{if(!e||t.length===0)return;let n=!0,r=t.map(e=>e.path),i=setInterval(()=>{for(let i of r)yt(wt(e,i)).then(e=>{if(!n)return;let r=t.find(e=>e.path===i);r&&e.etag!==r.etag&&u(e=>({...e,[i]:!0}))}).catch(()=>{})},ft);return()=>{n=!1,clearInterval(i)}},[e,t]);let D=(0,p.useCallback)(async(r,i)=>{if(!e)return;let a=t.find(e=>e.path===r);if(!a)return;c(e=>({...e,[r]:{state:`saving`,message:``}}));let o=a.etag;if(i)try{o=(await yt(wt(e,r))).etag}catch(e){c(t=>({...t,[r]:{state:`error`,message:`无法读取磁盘上的最新版本:${e instanceof Error?e.message:`未知错误`}`}}));return}try{let t=await yt(Ct(e,r),{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({path:r,content:a.content,baseEtag:o})});n(e=>e.map(e=>e.path===r?{...e,etag:t.meta.etag,dirty:!1}:e)),u(e=>({...e,[r]:!1})),c(e=>({...e,[r]:{state:`saved`,message:`已保存 ${Dt(t.meta.modifiedAt)}`}}))}catch(e){let t=e instanceof gt&&e.body.code===`stale`;c(n=>({...n,[r]:{state:t?`conflict`:`error`,message:e instanceof Error?e.message:`保存失败`}})),t&&u(e=>({...e,[r]:!0}))}},[e,t]),ne=(0,p.useCallback)(async t=>{if(e)try{let r=await yt(Ct(e,t));n(e=>e.map(e=>e.path===t?{...e,content:r.content,etag:r.meta.etag,dirty:!1}:e)),u(e=>({...e,[t]:!1})),c(e=>({...e,[t]:{state:`saved`,message:`已重新载入磁盘版本`}}))}catch(e){c(n=>({...n,[t]:{state:`error`,message:e instanceof Error?e.message:`重新载入失败`}}))}},[e]);return{open:t,activePath:r,active:t.find(e=>e.path===r),tabItems:(0,p.useMemo)(()=>t.map(e=>({id:e.path,label:e.name,title:e.path,closable:!0,badge:e.dirty?(0,m.jsx)(`span`,{className:`file-tab-badge`,"data-file-tab-dirty":e.path,"aria-label":`未保存`,children:`●`}):void 0})),[t]),openPaths:(0,p.useMemo)(()=>new Set(t.map(e=>e.path)),[t]),notice:a,setNotice:o,saves:s,external:l,openFile:ee,setActivePath:i,closeFile:E,updateBuffer:te,save:D,reload:ne,listings:d,expanded:h,toggleDirectory:w,loadDirectory:C,refreshTree:T,query:_,setQuery:v,hits:y,searching:x}}function Nt({workspaceId:e,files:t,onOpenFile:n}){let{open:r,openPaths:i,notice:a,setNotice:o,openFile:s,listings:c,expanded:l,toggleDirectory:u,loadDirectory:d,refreshTree:f,query:h,setQuery:g,hits:_,searching:v}=t,y=(0,p.useCallback)(e=>{n?n(e):s(e)},[n,s]),b=c[``],x=h.trim(),S=(0,p.useMemo)(()=>x?v||!_?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-searching":`1`,children:`搜索中…`}):_.hits.length===0?(0,m.jsxs)(`p`,{className:`rail-empty`,"data-file-search-empty":`1`,children:[`没有匹配「`,x,`」的文件。`]}):(0,m.jsxs)(m.Fragment,{children:[_.truncated&&(0,m.jsx)(`p`,{className:`file-search-note`,"data-file-search-partial":`1`,children:`结果不完整:搜索有上限,只列出了一部分。`}),_.skipped.length>0&&(0,m.jsxs)(`p`,{className:`file-search-note`,"data-file-search-skipped":`1`,children:[`已跳过 `,_.skipped.length,` 个目录(node_modules、.git 等)。`]}),(0,m.jsx)(`ul`,{className:`file-list`,children:_.hits.map(e=>(0,m.jsx)(`li`,{children:(0,m.jsxs)(`button`,{type:`button`,className:`file-row file-row-file`,"data-file-hit":e.path,"data-file-hit-open":i.has(e.path)?`1`:`0`,onClick:()=>y({path:e.path,name:e.name,text:!0}),children:[(0,m.jsx)(`span`,{className:`file-caret`,"aria-hidden":`true`}),(0,m.jsx)(`span`,{className:`file-name`,children:e.name}),i.has(e.path)&&(0,m.jsx)(`span`,{className:`file-open-mark`,"data-file-open-mark":e.path,title:`已经打开`,"aria-label":`已经打开`,children:`●`}),(0,m.jsx)(`span`,{className:`file-badge-path`,children:e.directory||`/`})]})},e.path))})]}):e?b===void 0?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-root-loading":`1`,children:`读取工作区…`}):`error`in b?(0,m.jsx)(jt,{error:b.error}):(0,m.jsxs)(m.Fragment,{children:[b.truncated&&(0,m.jsx)(`p`,{className:`file-search-note`,"data-file-truncated-root":`1`,children:`条目过多,只显示前一部分。`}),b.entries.length===0?(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-root-empty":`1`,children:`这个工作区是空的,没有文件也没有子目录。`}):(0,m.jsx)(`ul`,{className:`file-list`,children:b.entries.map(t=>t.kind===`directory`?(0,m.jsx)(Ot,{workspaceId:e,entryPath:t.path,name:t.name,listings:c,expanded:l,toggle:u,loadDirectory:d,openPaths:i,onOpen:e=>y(e),depth:0},t.path):(0,m.jsx)(kt,{entry:t,depth:0,openPaths:i,onOpen:e=>y(e)},t.path))})]}):(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-no-workspace":`1`,children:`先在左侧选一个工作区,这里会显示它的文件。`}),[x,v,_,e,b,c,l,u,d,y,i]);return(0,m.jsxs)(`div`,{className:`files-panel`,"data-files-panel":`1`,"data-files-open-count":r.length,children:[(0,m.jsxs)(`div`,{className:`files-search`,children:[(0,m.jsx)(`input`,{type:`search`,className:`files-search-input`,"data-file-search-input":`1`,value:h,placeholder:`按文件名搜索…`,"aria-label":`按文件名搜索`,onChange:e=>g(e.target.value)}),(0,m.jsx)(`button`,{type:`button`,className:`files-refresh`,"data-file-refresh":`1`,title:`重新读取目录`,"aria-label":`重新读取目录`,disabled:!e,onClick:()=>f(),children:`↻`})]}),a&&(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-notice":`1`,children:a}),(0,m.jsx)(`div`,{className:`files-tree`,"data-file-tree":x?`search`:`browse`,children:S}),r.length===0&&(0,m.jsx)(`p`,{className:`rail-empty`,"data-file-none-open":`1`,children:`点一个文本文件,它会在上面的标签栏开一个标签,正文显示在中间主画面。`}),r.length>0&&(0,m.jsxs)(`p`,{className:`files-hint`,"data-file-open-hint":`1`,children:[`已打开 `,r.length,` 个文件,正文显示在中间主画面;点上面的标签切换,点这里的行切换。`]}),(0,m.jsxs)(`p`,{className:`files-footnote`,"data-files-footnote":`1`,children:[`只支持纯文本(md / 代码),二进制文件不会打开。写入只允许覆盖已存在的文件,不新建,不删除。`,r.length>0&&` 当前打开 ${r.length} 个文件,占 ${Et(r.reduce((e,t)=>e+t.content.length,0))}。`]})]})}function Pt(e,t,n=88,r=200){let i=Math.max(0,Math.floor(e));if(!Number.isFinite(t)||t<=0)return{perRow:Math.max(1,i),width:n};if(i===0)return{perRow:1,width:n};if(i===1)return{perRow:1,width:Math.min(r,t)};let a=t/i;if(a>=n)return{perRow:i,width:Math.min(a,r)};for(let e=2;e<=i;e+=1){let a=Math.ceil(i/e),o=t/a;if(o>=n)return{perRow:a,width:Math.min(o,r)}}return{perRow:1,width:n}}function Ft(e,t){let n=Math.max(1,Math.floor(t)),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return r}function It(e,t,n=88,r=200){let[i,a]=(0,p.useState)(()=>Pt(e,0,n,r)),[o,s]=(0,p.useState)(null),c=(0,p.useCallback)(()=>{let i=Pt(e,t,n,r);s({count:e,containerWidth:t}),a(e=>e.perRow===i.perRow&&e.width===i.width?e:i)},[e,t,n,r]),l=(0,p.useRef)(t);(0,p.useEffect)(()=>{t<=0||l.current!==t&&(l.current=t,c())},[t,c]);let u=(0,p.useRef)(null);return(0,p.useEffect)(()=>{let t=u.current;u.current=e,t!==null&&e>t&&c()},[e,c]),{layout:i,frozen:o!==null&&o.count!==e,recompute:c}}function Lt({tabs:e,activeId:t,onSelect:n,onClose:r,ariaLabel:i,minWidth:a=88,maxWidth:o=200,prefix:s=`workspace-tab`,idPrefix:c=s,className:l}){let u=(0,p.useRef)(null),[d,f]=(0,p.useState)(0),{layout:h,frozen:g,recompute:_}=It(e.length,d,a,o);(0,p.useLayoutEffect)(()=>{let e=u.current;if(!e||typeof ResizeObserver>`u`)return;let t=()=>{let t=e.clientWidth;t>0&&f(t)};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let v=Ft(e,h.perRow),y=(e,t)=>t===void 0?{}:{[`data-${s}-${e}`]:t},b=e.filter(e=>e.kind===`file`).length,x=e.filter(e=>e.kind===`session`||!e.kind).length,S=t=>({[`data-${t}-strip`]:`1`,...g?{[`data-${t}-frozen`]:`1`}:{},[`data-${t}-count`]:t===`file-tab`?b:t===`session-tab`?x:e.length,[`data-${t}-min`]:a,[`data-${t}-max`]:o,[`data-${t}-per-row`]:h.perRow,[`data-${t}-width`]:Math.round(h.width*100)/100}),C=e=>{let t=e.relatedTarget;(!t||t.isConnected)&&_()},w=e=>{let t=e.relatedTarget;(!t||t.isConnected)&&_()};(0,p.useEffect)(()=>{if(!g)return;let e=u.current;if(!e)return;let t=t=>{let n=e.getBoundingClientRect();t.clientX>=n.left&&t.clientX<=n.right&&t.clientY>=n.top&&t.clientY<=n.bottom||_()};return document.addEventListener(`mousemove`,t,!0),()=>document.removeEventListener(`mousemove`,t,!0)},[g,_]);let T=[`${s}-strip`,`workspace-tab-strip`,`session-tab-strip`,`file-tab-strip`,l].filter(Boolean).join(` `);return(0,m.jsx)(`div`,{ref:u,className:T,role:`tablist`,"aria-label":i??`工作区标签页`,...y(`strip`,`1`),...S(`workspace-tab`),...S(`session-tab`),...S(`file-tab`),style:{"--tab-w":`${h.width}px`},onMouseEnter:C,onMouseLeave:w,children:v.map((i,a)=>(0,m.jsx)(`div`,{className:`${s}-row workspace-tab-row session-tab-row file-tab-row`,role:`presentation`,...y(`row`,a),children:i.map(i=>{let a=i.id===t||i.rawId!==void 0&&i.rawId===t,o=i.title??i.label,l=i.kind,u=i.rawId??i.id,d=e.findIndex(e=>e.id===i.id),f=[s,a?`${s}-active`:``,`workspace-tab`,a?`workspace-tab-active`:``,l?`${l}-tab`:``,l&&a?`${l}-tab-active`:``].filter(Boolean).join(` `);return(0,m.jsxs)(`div`,{role:`tab`,id:`${c}-${i.id}`,"aria-selected":a,"aria-controls":`${c}-panel-${i.id}`,tabIndex:a?0:-1,className:f,...y(`id`,u),...y(`index`,d),...a?y(`active`,`1`):{},...y(`closable`,i.closable===!1?`0`:`1`),...l?{"data-tab-kind":l}:{},...l===`session`||!l?{"data-session-tab-id":u,"data-session-tab-index":d,...a?{"data-session-tab-active":`1`}:{},"data-session-tab-closable":i.closable===!1?`0`:`1`}:{},...l===`file`?{"data-file-tab-id":u,"data-file-tab-index":d,...a?{"data-file-tab-active":`1`}:{},"data-file-tab-closable":i.closable===!1?`0`:`1`}:{},title:o,style:{width:h.width,boxSizing:`border-box`},onClick:()=>n?.(i.id),onAuxClick:e=>{e.button===1&&(e.preventDefault(),e.stopPropagation(),i.closable!==!1&&r&&r(i.id))},onMouseDown:e=>{e.button===1&&e.preventDefault()},onKeyDown:t=>{if(t.key!==`ArrowLeft`&&t.key!==`ArrowRight`)return;t.preventDefault();let r=t.key===`ArrowRight`?1:-1,a=e[(e.findIndex(e=>e.id===i.id)+r+e.length)%e.length];a&&n?.(a.id)},children:[i.icon&&(0,m.jsx)(`span`,{className:`${s}-icon workspace-tab-icon`,children:i.icon}),i.badge,(0,m.jsx)(`span`,{className:`${s}-label workspace-tab-label ${l?`${l}-tab-label`:``}`,...y(`label`,u),...l===`session`||!l?{"data-session-tab-label":u}:{},...l===`file`?{"data-file-tab-label":u}:{},children:i.label}),i.closable!==!1&&r&&(0,m.jsx)(`button`,{type:`button`,className:`${s}-close workspace-tab-close ${l?`${l}-tab-close`:``}`,...y(`close`,u),...l===`session`||!l?{"data-session-tab-close":u}:{},...l===`file`?{"data-file-tab-close":u}:{},onClick:e=>{e.stopPropagation(),r(i.id)},title:`关闭 ${o}`,"aria-label":`关闭 ${o}`,children:`×`})]},i.id)})},`row_${a}`))})}var Rt={pending:`未开始`,running:`运行中`,completed:`已完成`,failed:`失败`,skipped:`已跳过`,stopped:`已停止`},zt={running:`运行中`,completed:`全部完成`,failed:`中途失败`,stopped:`已停止`},Bt={pending:`○`,running:`◐`,completed:`✓`,failed:`✕`,skipped:`→`,stopped:`■`},Vt={stop:`失败即停`,skip:`失败跳过`,"retry:1":`失败重试 1 次`,"retry:2":`失败重试 2 次`,"retry:3":`失败重试 3 次`};function Ht(e){let t=Math.max(0,Math.round(e/1e3)),n=t%60,r=Math.floor(t/60)%60,i=Math.floor(t/3600),a=e=>String(e).padStart(2,`0`);return i>0?`${i}h ${a(r)}:${a(n)}`:`${r}:${a(n)}`}function Ut(e){let t=e.status===`running`,[n,r]=(0,p.useState)(0);(0,p.useEffect)(()=>{if(!t)return;r(Date.now());let e=window.setInterval(()=>r(Date.now()),200);return()=>window.clearInterval(e)},[t,e.startedAt]);let i=Date.parse(e.startedAt);if(Number.isFinite(i)){if(!t){let t=Date.parse(e.finishedAt??``);return Number.isFinite(t)?t-i:void 0}return n>0?n-i:void 0}}function Wt({step:e,names:t}){let n=e.attempts.length,r=e.attempts[n-1],i=t.get(e.agentId)||e.agentId,a=e.label?.trim()||i;return(0,m.jsxs)(`li`,{className:`workflow-step workflow-step-${e.status}`,"data-workflow-step":e.index+1,"data-workflow-step-status":e.status,"data-workflow-step-agent":e.agentId,"data-workflow-step-attempts":n,children:[(0,m.jsx)(`span`,{className:`workflow-step-mark`,"aria-hidden":`true`,children:Bt[e.status]}),(0,m.jsxs)(`span`,{className:`workflow-step-n`,"data-workflow-step-n":e.index+1,children:[`第 `,e.index+1,` 步`]}),(0,m.jsx)(`strong`,{className:`workflow-step-name`,"data-workflow-step-name":e.index+1,children:a}),(0,m.jsx)(`span`,{className:`workflow-step-agent`,"data-workflow-step-agent-name":e.index+1,children:i}),n>1&&(0,m.jsxs)(`span`,{className:`workflow-step-tries`,"data-workflow-step-retried":e.index+1,children:[`尝试 `,n,` 次`]}),(0,m.jsx)(`span`,{className:`workflow-step-status`,"data-workflow-step-status-text":e.index+1,children:Rt[e.status]}),e.status===`failed`&&r?.error&&(0,m.jsx)(`span`,{className:`workflow-step-why`,"data-workflow-step-error":e.index+1,children:r.error})]})}function Gt({workflow:e,names:t,onStop:n,stopping:r}){let i=e.status===`running`,a=Ut(e),o=e.steps.filter(e=>e.status===`completed`||e.status===`skipped`).length;return(0,m.jsxs)(`article`,{className:`workflow-card workflow-${e.status}`,"data-workflow-card":e.id,"data-workflow-status":e.status,"data-workflow-steps":e.steps.length,"data-workflow-current":e.currentIndex+1,children:[(0,m.jsxs)(`div`,{className:`workflow-head`,children:[(0,m.jsx)(`span`,{className:`workflow-badge`,children:`编排`}),(0,m.jsx)(`strong`,{className:`workflow-summary`,"data-workflow-summary":e.id,children:e.summary}),(0,m.jsxs)(`span`,{className:`workflow-progress`,"data-workflow-progress":e.id,children:[o,`/`,e.steps.length]}),(0,m.jsx)(`span`,{className:`workflow-status`,"data-workflow-status-text":e.id,children:zt[e.status]??e.status}),(0,m.jsx)(`span`,{className:`workflow-policy`,"data-workflow-policy":e.onFailure,children:Vt[e.onFailure]??e.onFailure}),a!==void 0&&(0,m.jsxs)(`span`,{className:`workflow-clock`,"data-workflow-elapsed":e.id,children:[i?`已运行 `:`用时 `,Ht(a)]}),i&&(0,m.jsx)(`button`,{type:`button`,className:`workflow-stop`,"data-workflow-stop":e.id,disabled:r,onClick:n,children:`停止`})]}),(0,m.jsx)(`ol`,{className:`workflow-steps`,"data-workflow-step-count":e.steps.length,children:e.steps.map(e=>(0,m.jsx)(Wt,{step:e,names:t},e.index))})]})}var j=[{id:`agents`,label:`智能体与创建`,icon:`🤖`,title:`Agent 智能体系统与创建指南`,description:`了解 Super Pi 的角色设定机制,以及如何一步步创建你自己的自定义 Agent。`},{id:`workspace`,label:`工作区与会话`,icon:`🗂️`,title:`工作区与多会话管理`,description:`高效管理本地代码仓库、独立会话标签页与多模态图文输入。`},{id:`subagents`,label:`子任务与编排`,icon:`⚡`,title:`子智能体委托与顺序编排使用指南`,description:`让主 Agent 安排分工、交接结果,并查看进度、处理失败与停止任务。`},{id:`model-policy`,label:`模型使用规则`,icon:`🔒`,title:`模型过滤规则(按工作区限制可用模型)`,description:`用正则规则限定哪些工作区只能用哪些模型,防止敏感代码被发到不该用的模型上。`},{id:`memory`,label:`主动记忆`,icon:`🧠`,title:`主动记忆与技能经验`,description:`让 Super Pi 在得到明确证据后保存长期事实、用户偏好和可复用工作方法。`},{id:`knowledge`,label:`知识库`,icon:`📚`,title:`工作区知识库`,description:`导入规范与设计文档,让 Agent 检索原文作答,而不是凭记忆编造。`},{id:`tools`,label:`侧边栏工具`,icon:`🧩`,title:`全局工具:开发、导出与导入`,description:`让 Agent 写一个工具,用一个文件分享给别人,导入前先看清它会做什么。`},{id:`cli`,label:`指令与快捷键`,icon:`⌨️`,title:`Pi 终端命令与键盘快捷操作`,description:`利用 Pi 终端斜杠指令控制服务生命周期,掌握常用快捷键提升效率。`},{id:`themes`,label:`外观与主题`,icon:`🎨`,title:`界面主题与个性化显示`,description:`内置 4 款专业高对比度主题,针对不同环境提供舒适体验。`}];function Kt({onClose:e}){let[t,n]=(0,p.useState)(`agents`);(0,p.useEffect)(()=>{let t=t=>{t.key===`Escape`&&!t.defaultPrevented&&e()};return window.addEventListener(`keydown`,t),()=>window.removeEventListener(`keydown`,t)},[e]);let r=j.find(e=>e.id===t)??j[0];return(0,m.jsx)(`div`,{className:`overlay`,onClick:e,children:(0,m.jsxs)(`div`,{className:`picker settings-window help-window`,"data-help-window":`1`,onClick:e=>e.stopPropagation(),role:`dialog`,"aria-modal":`true`,"aria-labelledby":`help-window-title`,children:[(0,m.jsxs)(`div`,{className:`settings-window-head help-window-head`,children:[(0,m.jsxs)(`div`,{className:`help-head-title`,children:[(0,m.jsx)(`h2`,{id:`help-window-title`,children:`使用指南与帮助`}),(0,m.jsx)(`span`,{className:`help-badge`,children:`v0.1.x`})]}),(0,m.jsx)(`button`,{type:`button`,className:`help-close-button`,"aria-label":`关闭指南`,onClick:e,children:`✕`})]}),(0,m.jsxs)(`div`,{className:`settings-window-body help-window-body`,children:[(0,m.jsxs)(`nav`,{className:`settings-nav help-nav`,role:`tablist`,"aria-orientation":`vertical`,"aria-label":`使用指南分类`,children:[(0,m.jsx)(`div`,{className:`settings-nav-label`,children:`目录`}),j.map(e=>(0,m.jsxs)(`button`,{type:`button`,id:`help-tab-${e.id}`,className:`settings-category help-category`,role:`tab`,"aria-selected":e.id===r.id,"aria-controls":`help-panel`,tabIndex:e.id===r.id?0:-1,onClick:()=>n(e.id),children:[(0,m.jsx)(`span`,{className:`help-tab-icon`,"aria-hidden":`true`,children:e.icon}),(0,m.jsx)(`span`,{children:e.label})]},e.id))]}),(0,m.jsxs)(`div`,{id:`help-panel`,className:`settings-panel help-panel`,role:`tabpanel`,"aria-labelledby":`help-tab-${r.id}`,children:[(0,m.jsxs)(`div`,{className:`help-panel-header`,children:[(0,m.jsx)(`h3`,{className:`help-panel-title`,children:r.title}),(0,m.jsx)(`p`,{className:`help-panel-desc`,children:r.description})]}),(0,m.jsxs)(`div`,{className:`help-panel-content`,children:[r.id===`agents`&&(0,m.jsx)(Yt,{}),r.id===`workspace`&&(0,m.jsx)(Xt,{}),r.id===`subagents`&&(0,m.jsx)(Qt,{}),r.id===`model-policy`&&(0,m.jsx)(Zt,{}),r.id===`memory`&&(0,m.jsx)(qt,{}),r.id===`knowledge`&&(0,m.jsx)(Jt,{}),r.id===`tools`&&(0,m.jsx)(M,{}),r.id===`cli`&&(0,m.jsx)($t,{}),r.id===`themes`&&(0,m.jsx)(en,{})]}),(0,m.jsx)(`div`,{className:`picker-actions help-actions`,children:(0,m.jsx)(`button`,{type:`button`,className:`picker-cancel`,onClick:e,children:`关闭`})})]})]})]})})}function qt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`🧠 它会记住什么?`}),(0,m.jsx)(`p`,{children:`用户档案保存跨工作区的长期偏好;工作区事实保存环境与项目约定;技能保存已经验证过的步骤、命令和坑。临时结果、秘密和未验证的失败不会保存。`})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`主动保存与后台复盘`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[`主 Agent 可以使用 `,(0,m.jsx)(`code`,{children:`memory`}),` 和 `,(0,m.jsx)(`code`,{children:`skill_manage`}),` 工具主动保存;说“记住了”本身不会写入。`]}),(0,m.jsx)(`li`,{children:`每隔若干回合,系统会在独立会话中复盘最近证据。复盘不能读写工作区文件、执行命令或派发任务。`}),(0,m.jsx)(`li`,{children:`读取记忆和历史只返回带来源的参考数据,不能把记忆中的文字当作新的指令。`})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`权限与审批`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`子 Agent 可以读取相关记忆,但没有写入权限。`}),(0,m.jsx)(`li`,{children:`后台复盘新增记忆可以直接保存;修改或删除旧条目会进入设置中的待审批列表。`}),(0,m.jsx)(`li`,{children:`在“设置 → 主动记忆”可以开关功能、调整复盘间隔、编辑条目和批准提案。`})]})]})]})}function Jt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`📚 它解决什么问题?`}),(0,m.jsx)(`p`,{children:`Agent 默认靠训练时的记忆回答项目里的问题。知识库让你把规范、设计稿、会议纪要导入工作区,Agent 在回答前检索这些文档的原文片段,而不是凭印象编造。它与“主动记忆”不同:记忆是 Agent 自己沉淀的事实和技能,知识库只有你能写入。`})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`怎么用`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`在“设置 → 知识库”里点“浏览工作区文件…”,在弹出的浏览器里挑一个 Markdown 或文本文件,挑完即导入。也可以直接粘贴正文,或手动填写相对路径。`}),(0,m.jsx)(`li`,{children:`同一个文件内容改了再导入就是更新,不会留下旧版本;同样的内容换个标题再导入也不会变成两篇。`}),(0,m.jsx)(`li`,{children:`面板里的“试检索”会用和 Agent 相同的检索器回答,方便你在交给 Agent 之前先确认它找得到。`}),(0,m.jsxs)(`li`,{children:[`从下一条消息开始生效。Agent 使用 `,(0,m.jsx)(`code`,{children:`knowledge_search`}),` 检索,只拿得到片段和来源,拿不到整篇文档。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`语义检索是内置的`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[`关键词检索始终可用。语义检索让“撤销购买”也能找到写着“取消订单”的段落,它靠一个`,(0,m.jsx)(`strong`,{children:`随安装包一起分发`}),`的中文模型在本机运行,不需要额外下载、不需要联网、也不会把文档发到任何服务。`]}),(0,m.jsxs)(`li`,{children:[`第一次检索时加载模型(约一秒),之后一直可用。设 `,(0,m.jsx)(`code`,{children:`SUPER_PI_KNOWLEDGE_EMBEDDINGS=0`}),` 可以彻底关闭语义检索,只保留关键词检索。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`它不知道的事`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsx)(`li`,{children:`知识库只会检索它有的东西。文档里没写的,它检索出来也不会有——所以导入什么,直接决定它能回答什么。`}),(0,m.jsx)(`li`,{children:`语义检索总会返回“最接近的那一段”,即使这一段其实并不回答问题。检索结果带一个分数,低分意味着这可能只是最像的一条,Agent 会被要求先核对再使用。`}),(0,m.jsx)(`li`,{children:`Agent 对知识库只读,不能写入或删除;导入和删除只有你能做。`})]})]})]})}function Yt(){return(0,m.jsxs)(`div`,{className:`help-section-group`,children:[(0,m.jsxs)(`div`,{className:`help-card highlight`,children:[(0,m.jsx)(`h4`,{children:`💡 Agent 是什么?`}),(0,m.jsxs)(`p`,{children:[`在 Super Pi 中,`,(0,m.jsx)(`strong`,{children:`Agent(智能体)`}),` 是一套独立的「角色提示词设定与能力配置」。 通过切换 Agent,模型将扮演专精工程师、系统架构师、代码审查员或规划专家等不同角色,遵循指定的原则来协助你完成任务。`]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`📁 Agent 文件存放在哪里?`}),(0,m.jsxs)(`ul`,{className:`help-list`,children:[(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`内置 Agent 目录:`}),`项目根目录下的 `,(0,m.jsx)(`code`,{children:`agents/`}),` 文件夹,例如 `,(0,m.jsx)(`code`,{children:`agents/coder.md`}),`、`,(0,m.jsx)(`code`,{children:`agents/planner.md`}),`、`,(0,m.jsx)(`code`,{children:`agents/reviewer.md`}),` 等。`]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`用户自定义持久化目录:`}),`用户主目录下的 `,(0,m.jsx)(`code`,{children:`~/.super-pi/agents/`}),`,放入此目录的文件在所有项目和更新中均持久有效。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-card`,children:[(0,m.jsx)(`h4`,{children:`🚀 如何创建一个全新的自定义 Agent?`}),(0,m.jsxs)(`div`,{className:`help-steps`,children:[(0,m.jsxs)(`div`,{className:`help-step`,children:[(0,m.jsx)(`span`,{className:`step-num`,children:`1`}),(0,m.jsxs)(`div`,{className:`step-body`,children:[(0,m.jsx)(`strong`,{children:`创建 Markdown 文件`}),(0,m.jsxs)(`p`,{children:[`在 `,(0,m.jsx)(`code`,{children:`agents/`}),` 或 `,(0,m.jsx)(`code`,{children:`~/.super-pi/agents/`}),` 目录下新建文件,例如 `,(0,m.jsx)(`code`,{children:`architect.md`}),`。`]})]})]}),(0,m.jsxs)(`div`,{className:`help-step`,children:[(0,m.jsx)(`span`,{className:`step-num`,children:`2`}),(0,m.jsxs)(`div`,{className:`step-body`,children:[(0,m.jsx)(`strong`,{children:`编写元数据与系统提示词`}),(0,m.jsx)(`p`,{children:`文件开头使用 YAML Frontmatter 声明名称和描述,正文编写设定提示词:`}),(0,m.jsx)(`pre`,{className:`help-code-block`,children:(0,m.jsx)(`code`,{children:`---
26
26
  name: 系统架构师
27
27
  description: 专注于系统架构拆解、模块解耦与技术选型分析
28
28
  ---
@@ -8,7 +8,7 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-pD4FaDWr.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-mAu39Bra.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-DDkbPYLz.css">
13
13
  </head>
14
14
  <body>
@@ -219,6 +219,57 @@ export declare class RunService {
219
219
  * the moment Pi holds it.
220
220
  */
221
221
  onPiQueue(conversationId: string, snapshot: PiQueueSnapshot): void;
222
+ /**
223
+ * The message a delivery is about, resolved by the text Pi announced.
224
+ *
225
+ * WHY THE TEXT AND NOT THE POSITION. A message that was STEERED is `sending` here - claimed, so the
226
+ * user cannot withdraw it - and the oldest `sending` message is the one Pi picked up, because Pi's
227
+ * steering queue is FIFO. That is the old rule and it still holds. A message that was handed to Pi's
228
+ * FOLLOW-UP queue is NOT `sending`: Super Pi keeps it in its own list so the user can still withdraw
229
+ * it, and a conversation can hold a `waiting` message and a `sending` one at the same time. For that
230
+ * one, position is the wrong question and the text is the right one.
231
+ *
232
+ * THE TEXT IS THE SAME TEXT ON BOTH SIDES. Pi matches its own queue by `indexOf` on the message text
233
+ * (`agent-session.ts:1123`), so two messages with identical words are ONE entry to Pi and the first of
234
+ * them to Super Pi as well. Resolving by text is therefore not a guess that could pick a different
235
+ * message than Pi did - it is the same comparison Pi itself made.
236
+ *
237
+ * A `sending` message wins over a `waiting` one with the same text, because Pi drains its steering
238
+ * queue before its follow-up queue, so the steering message is the one that was delivered first.
239
+ */
240
+ private matchDeliveredMessage;
241
+ /**
242
+ * Hands a waiting message to Pi's follow-up queue, so the run that is already going answers it.
243
+ *
244
+ * WHY THIS EXISTS. A message typed while the agent was busy used to wait for the current turn to END
245
+ * and then be answered by a turn of its own - a fresh `prompt()`, which runs Pi's `_prepareRun`, which
246
+ * emits `before_agent_start`. Super Pi's memory extension answers that hook with an HTTP search and
247
+ * patches the system prompt with what came back, so every queued message paid for a round trip to the
248
+ * memory endpoint and for a prompt larger than the one before it. A follow-up is delivered by Pi's
249
+ * agent loop inside the running turn, so it pays for neither.
250
+ *
251
+ * IT IS AN OPTIMISATION, NOT A COMMITMENT. The message stays in Super Pi's own waiting list, so it is
252
+ * still the user's to withdraw and still the user's to interrupt, and if Pi does not deliver it - the
253
+ * run was aborted, the queues were cleared - the message falls back to being answered by a turn of its
254
+ * own. Nothing is lost either way, which is what makes it safe to try.
255
+ */
256
+ private submitToPiQueue;
257
+ /**
258
+ * Empties Pi's steering and follow-up queues for a conversation.
259
+ *
260
+ * WHY A HOST HAS TO DO THIS. Pi delivers whatever is left in those queues when the run ends, and it
261
+ * cannot take back a single message - `clear_queue` is the only queue command it has. So whenever
262
+ * Super Pi takes a message back, the queues have to be emptied, or Pi answers words the user believes
263
+ * they took back.
264
+ *
265
+ * NOTHING IS LOST. Every message Pi was holding is still in Super Pi's own waiting list, and the run's
266
+ * loop answers those as turns of their own.
267
+ *
268
+ * THE STEERS COME BACK. Emptying the queues also empties the steering queue, and a steer is a message
269
+ * the user asked to be answered NOW - so the ones Pi had not delivered go back in, and clearing the
270
+ * queues for one message does not silently un-steer another.
271
+ */
272
+ private clearPiQueues;
222
273
  /**
223
274
  * Opens a turn on a message that is ALREADY IN THE TRANSCRIPT.
224
275
  *
@@ -285,6 +285,11 @@ export class RunService {
285
285
  console.warn(`Super Pi: auto-trigger for background exec result failed: ${error instanceof Error ? error.message : "unknown error"}`);
286
286
  });
287
287
  }
288
+ else {
289
+ // THE CONVERSATION IS BUSY, so the run that is going answers this result itself - through
290
+ // Pi's own follow-up queue, so the result does not have to wait for a turn of its own.
291
+ await this.submitToPiQueue(task.conversationId, queued);
292
+ }
288
293
  }
289
294
  catch (error) {
290
295
  console.warn(`Super Pi: exec result could not be appended as a tool message: ${error instanceof Error ? error.message : "unknown error"}`);
@@ -541,6 +546,118 @@ export class RunService {
541
546
  onPiQueue(conversationId, snapshot) {
542
547
  this.broadcaster.broadcast({ type: "pi.queue", conversationId, ...snapshot });
543
548
  }
549
+ /**
550
+ * The message a delivery is about, resolved by the text Pi announced.
551
+ *
552
+ * WHY THE TEXT AND NOT THE POSITION. A message that was STEERED is `sending` here - claimed, so the
553
+ * user cannot withdraw it - and the oldest `sending` message is the one Pi picked up, because Pi's
554
+ * steering queue is FIFO. That is the old rule and it still holds. A message that was handed to Pi's
555
+ * FOLLOW-UP queue is NOT `sending`: Super Pi keeps it in its own list so the user can still withdraw
556
+ * it, and a conversation can hold a `waiting` message and a `sending` one at the same time. For that
557
+ * one, position is the wrong question and the text is the right one.
558
+ *
559
+ * THE TEXT IS THE SAME TEXT ON BOTH SIDES. Pi matches its own queue by `indexOf` on the message text
560
+ * (`agent-session.ts:1123`), so two messages with identical words are ONE entry to Pi and the first of
561
+ * them to Super Pi as well. Resolving by text is therefore not a guess that could pick a different
562
+ * message than Pi did - it is the same comparison Pi itself made.
563
+ *
564
+ * A `sending` message wins over a `waiting` one with the same text, because Pi drains its steering
565
+ * queue before its follow-up queue, so the steering message is the one that was delivered first.
566
+ */
567
+ matchDeliveredMessage(conversationId, text) {
568
+ if (!text)
569
+ return undefined;
570
+ const waiting = this.store.listWaitingMessages(conversationId);
571
+ const match = waiting.find((item) => item.delivery === "sending" && composePrompt(item) === text)
572
+ ?? waiting.find((item) => item.delivery === "waiting" && composePrompt(item) === text);
573
+ return match?.id;
574
+ }
575
+ /**
576
+ * Hands a waiting message to Pi's follow-up queue, so the run that is already going answers it.
577
+ *
578
+ * WHY THIS EXISTS. A message typed while the agent was busy used to wait for the current turn to END
579
+ * and then be answered by a turn of its own - a fresh `prompt()`, which runs Pi's `_prepareRun`, which
580
+ * emits `before_agent_start`. Super Pi's memory extension answers that hook with an HTTP search and
581
+ * patches the system prompt with what came back, so every queued message paid for a round trip to the
582
+ * memory endpoint and for a prompt larger than the one before it. A follow-up is delivered by Pi's
583
+ * agent loop inside the running turn, so it pays for neither.
584
+ *
585
+ * IT IS AN OPTIMISATION, NOT A COMMITMENT. The message stays in Super Pi's own waiting list, so it is
586
+ * still the user's to withdraw and still the user's to interrupt, and if Pi does not deliver it - the
587
+ * run was aborted, the queues were cleared - the message falls back to being answered by a turn of its
588
+ * own. Nothing is lost either way, which is what makes it safe to try.
589
+ */
590
+ async submitToPiQueue(conversationId, message) {
591
+ if (!this.runtime.followUp)
592
+ return;
593
+ // NOTHING TO FOLLOW UP ON. With no turn running, Pi would hold the message until the NEXT one,
594
+ // which is a later answer than the one the message is waiting for.
595
+ if (!this.activeRuns.has(conversationId))
596
+ return;
597
+ // AN INTERRUPT IS ALREADY STARTING. It aborts the run and empties Pi's queues, so a message handed
598
+ // to Pi now would be taken back a moment later. It waits in the list for the next run, exactly as it
599
+ // did before.
600
+ if (this.interruptTargets.has(conversationId))
601
+ return;
602
+ // A MESSAGE THAT IS A COMMAND IS NOT QUEUED. Pi expands a skill command or a prompt template before
603
+ // it queues the text (`agent-session.ts:2221-2222`), and it REFUSES an extension command outright.
604
+ // An expanded text is not the text Super Pi has, so the delivery could not be resolved back to its
605
+ // message - the answer would land under the wrong question. Such a message waits for a turn of its
606
+ // own, where it is the prompt and the question and the answer are the same thing.
607
+ if (composePrompt(message).startsWith("/"))
608
+ return;
609
+ try {
610
+ await this.runtime.followUp(conversationId, composePrompt(message), message.images);
611
+ }
612
+ catch (error) {
613
+ // THE FALLBACK IS THE OLD BEHAVIOUR, so a refusal here is not a failure: the message stays in the
614
+ // waiting list and the run's loop answers it with a turn of its own.
615
+ console.warn(`Super Pi: handing a queued message to Pi's follow-up queue failed: ${error instanceof Error ? error.message : "unknown error"}`);
616
+ }
617
+ }
618
+ /**
619
+ * Empties Pi's steering and follow-up queues for a conversation.
620
+ *
621
+ * WHY A HOST HAS TO DO THIS. Pi delivers whatever is left in those queues when the run ends, and it
622
+ * cannot take back a single message - `clear_queue` is the only queue command it has. So whenever
623
+ * Super Pi takes a message back, the queues have to be emptied, or Pi answers words the user believes
624
+ * they took back.
625
+ *
626
+ * NOTHING IS LOST. Every message Pi was holding is still in Super Pi's own waiting list, and the run's
627
+ * loop answers those as turns of their own.
628
+ *
629
+ * THE STEERS COME BACK. Emptying the queues also empties the steering queue, and a steer is a message
630
+ * the user asked to be answered NOW - so the ones Pi had not delivered go back in, and clearing the
631
+ * queues for one message does not silently un-steer another.
632
+ */
633
+ async clearPiQueues(conversationId) {
634
+ if (!this.runtime.clearQueues)
635
+ return;
636
+ let cleared;
637
+ try {
638
+ cleared = await this.runtime.clearQueues(conversationId);
639
+ }
640
+ catch (error) {
641
+ console.warn(`Super Pi: clearing Pi's queues for ${conversationId} failed: ${error instanceof Error ? error.message : "unknown error"}`);
642
+ return;
643
+ }
644
+ for (const text of cleared.steering ?? []) {
645
+ // A STEER NEEDS A TURN TO GO INTO. With no turn running there is nothing to steer, and the message
646
+ // is released back to `waiting` when the run ends anyway - so it is answered as a turn of its own.
647
+ if (this.runtime.canSteer && !this.runtime.canSteer(conversationId))
648
+ continue;
649
+ const message = this.store.listWaitingMessages(conversationId)
650
+ .find((item) => item.delivery === "sending" && composePrompt(item) === text);
651
+ if (!message)
652
+ continue;
653
+ try {
654
+ await this.runtime.steer?.(conversationId, text, message.images);
655
+ }
656
+ catch (error) {
657
+ console.warn(`Super Pi: putting a steered message back failed: ${error instanceof Error ? error.message : "unknown error"}`);
658
+ }
659
+ }
660
+ }
544
661
  /**
545
662
  * Opens a turn on a message that is ALREADY IN THE TRANSCRIPT.
546
663
  *
@@ -825,18 +942,28 @@ export class RunService {
825
942
  * cleared rather than reassigned for the same reason. `thinkingBlock` continues numbering on purpose:
826
943
  * it is a global fold sequence, and restarting it would make the browser render two folds numbered 0.
827
944
  */
828
- const onSteerDelivered = () => {
829
- enqueueTools("a steer being delivered", async () => {
945
+ const onSteerDelivered = (text) => {
946
+ enqueueTools("a queued message being delivered", async () => {
830
947
  const previous = assistant;
831
- // Checkpoint first: the message being left behind is complete - Pi drains steering at a turn
948
+ // Checkpoint first: the message being left behind is complete - Pi drains its queues at a turn
832
949
  // boundary - and this is the last chance to write it durably with the lane it belongs to.
833
950
  await this.store.checkpointMessage(conversation.id, previous.id);
834
- const delivered = await this.store.deliverWaitingMessage(conversation.id, agentIdentity);
951
+ // WHICH MESSAGE THIS DELIVERY IS ABOUT. Pi announces the text it picked up, and Pi itself matches
952
+ // its own queue by that text (`agent-session.ts:1123`), so the same text resolves to the same
953
+ // message on both sides - including when two messages carry identical words, where both sides
954
+ // pick the first. A message handed to Pi's FOLLOW-UP queue was never claimed here: it is still
955
+ // `waiting`, so the user can still withdraw it, and a conversation can hold a `waiting` and a
956
+ // `sending` message at the same time. Position cannot answer that one; the text can.
957
+ //
958
+ // WHEN THE TEXT MATCHES NOTHING the old position-based answer is used, which is the steer path's
959
+ // proven behaviour: the message Pi is holding is the one Pi picked up. A text that matches
960
+ // nothing AND has no `sending` message is a delivery of a message Super Pi no longer has - one
961
+ // the user withdrew - and there is then nothing to open an answer to.
962
+ const target = this.matchDeliveredMessage(conversation.id, text);
963
+ const delivered = await this.store.deliverWaitingMessage(conversation.id, agentIdentity, target);
835
964
  // NOTHING TO DELIVER MEANS NOTHING TO SPLIT. Pi may announce a user `message_start` that this
836
965
  // server never handed it - the prompt that opened the turn is the obvious case - and inventing an
837
- // assistant message for one of those would open an empty bubble before every single turn. The
838
- // question "was anything `sending`?" is the server's own state, not something the event decides,
839
- // which is the same reason the delivery is matched by position rather than by text.
966
+ // assistant message for one of those would open an empty bubble before every single turn.
840
967
  if (!delivered)
841
968
  return [];
842
969
  const next = delivered.assistant;
@@ -978,6 +1105,13 @@ export class RunService {
978
1105
  for (;;) {
979
1106
  if (controller.signal.aborted)
980
1107
  break;
1108
+ // PI'S QUEUES ARE EMPTIED BEFORE THE NEXT MESSAGE IS TAKEN. A message handed to Pi's follow-up
1109
+ // queue is delivered by the agent loop inside the run that has just ended - Pi drains that queue
1110
+ // before it settles - so whatever is STILL in the queue when the turn ended was not delivered.
1111
+ // Emptying it gives those messages back: they are still in the waiting list, and the loop below
1112
+ // answers them with a turn of their own, which is the behaviour there was before Pi's queue was
1113
+ // used at all. Nothing is lost, and nothing is delivered twice.
1114
+ await this.clearPiQueues(conversation.id);
981
1115
  // Taken AND opened, in that order, by two separate writes. The take is what bounds this loop -
982
1116
  // every pass strictly shrinks the waiting set - and it is what makes a crash honest, because a
983
1117
  // server that dies between the two has a message that is neither waiting nor answered, and the
@@ -1045,6 +1179,10 @@ export class RunService {
1045
1179
  await this.store.markMessageWaiting(conversationId, message.id);
1046
1180
  this.broadcaster.broadcastState();
1047
1181
  }
1182
+ // The turn that is running answers this message itself, through Pi's own follow-up queue, rather
1183
+ // than by a turn of its own once the running one is over. The message stays in the waiting list
1184
+ // either way, so it is still the user's to withdraw - Pi is holding a copy, not the only copy.
1185
+ await this.submitToPiQueue(conversationId, message);
1048
1186
  return;
1049
1187
  }
1050
1188
  const controller = new AbortController();
@@ -1199,6 +1337,12 @@ export class RunService {
1199
1337
  try {
1200
1338
  if (this.activeRuns.has(conversationId))
1201
1339
  await this.stopRun(conversationId);
1340
+ // PI'S QUEUES ARE EMPTIED BEFORE THE SELECTED MESSAGE IS ANSWERED ON ITS OWN. The message may be
1341
+ // in Pi's follow-up queue - it was handed over when it was written - and Pi would deliver it when
1342
+ // the aborted run ended. The message is about to be answered as a turn of its own, so the queue
1343
+ // copy has to go: a message answered twice is worse than one answered once, and the transcript
1344
+ // would show the same words under two answers.
1345
+ await this.clearPiQueues(conversationId);
1202
1346
  await this.awaitRunSlotReleased(conversationId);
1203
1347
  await this.processWaitingMessage(conversationId, messageId);
1204
1348
  }
@@ -1300,6 +1444,12 @@ export class RunService {
1300
1444
  async cancelQueuedUnsafe(conversationId, messageId) {
1301
1445
  if (this.interruptTargets.get(conversationId) === messageId)
1302
1446
  throw new Error("这条消息正在打断并回答,无法撤回");
1447
+ // PI'S QUEUES ARE EMPTIED BEFORE THE MESSAGE IS TAKEN BACK. A message that was handed to Pi's
1448
+ // follow-up queue is in Pi's hands as well as in Super Pi's, and Pi would deliver it when the run
1449
+ // ends - the model would answer words the user believes they withdrew. Pi cannot take back one
1450
+ // message, so its queues are emptied; the other messages Pi was holding are still in Super Pi's
1451
+ // waiting list and are answered as turns of their own.
1452
+ await this.clearPiQueues(conversationId);
1303
1453
  await this.store.cancelWaitingMessage(conversationId, messageId);
1304
1454
  this.broadcaster.broadcastState();
1305
1455
  }
@@ -174,6 +174,31 @@ export interface PiSessionLike {
174
174
  images?: unknown[];
175
175
  streamingBehavior?: "steer" | "followUp";
176
176
  }): Promise<void>;
177
+ /**
178
+ * Queues a message to be answered AFTER the current run finishes its pending work.
179
+ *
180
+ * PI'S OWN FOLLOW-UP QUEUE, and the reason a queued message does not need a turn of its own. A
181
+ * `prompt()` runs `_prepareRun`, which emits `before_agent_start` - and Super Pi's memory extension
182
+ * answers that hook with an HTTP search whose result is patched into the system prompt. So a fresh
183
+ * prompt pays a round trip to the memory endpoint and a bigger prompt; a follow-up is delivered by
184
+ * the agent loop inside the run and pays neither.
185
+ *
186
+ * Optional because a test double has no queues; its absence means "this session cannot hold a
187
+ * message for later", and the caller falls back to answering it as a turn of its own.
188
+ */
189
+ followUp?(message: string, images?: unknown[]): Promise<"queued" | "handled">;
190
+ /**
191
+ * Removes every message Pi is holding in its steering and follow-up queues, and returns what was
192
+ * removed.
193
+ *
194
+ * PI CANNOT TAKE BACK ONE MESSAGE - `clear_queue` is the only queue command it has - so this is
195
+ * how a host says "none of these are wanted any more". The returned texts are the queue as it
196
+ * was, which is what lets a host put back the ones that should still be answered.
197
+ */
198
+ clearQueue?(): {
199
+ steering: string[];
200
+ followUp: string[];
201
+ };
177
202
  /** Aborts the current operation and waits for the session to become idle. */
178
203
  abort(): Promise<void>;
179
204
  /** Subscribes to session events. Returns the unsubscribe function. */
@@ -236,6 +236,32 @@ export interface AgentRuntime {
236
236
  */
237
237
  steer?(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
238
238
  canSteer?(conversationId: string): boolean;
239
+ /**
240
+ * Hands one message to the Pi that is running RIGHT NOW, to be answered after the current run
241
+ * finishes its pending work.
242
+ *
243
+ * PI'S OWN FOLLOW-UP QUEUE, and the difference from a steer is the difference in WHEN. A steer is
244
+ * injected at the next turn boundary of the running turn; a follow-up waits until the agent would
245
+ * otherwise stop. Both are delivered by the agent loop inside the same run, which is what makes
246
+ * them cheap: neither goes through `_prepareRun`, so neither re-emits `before_agent_start` and
247
+ * neither pays for the memory search and the system-prompt patch that hook does.
248
+ *
249
+ * A message handed over this way is still Super Pi's to cancel, because Super Pi keeps it in its
250
+ * own waiting list - Pi is holding a copy, not the only copy.
251
+ */
252
+ followUp?(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
253
+ /**
254
+ * Empties Pi's steering and follow-up queues, and returns what was in them.
255
+ *
256
+ * WHY A HOST NEEDS THIS. Pi cannot take back a single queued message, and it will deliver whatever
257
+ * is left in those queues when the run ends. So whenever Super Pi takes a message back - the user
258
+ * withdrew it, or the user asked for it to be answered on its own - the queues have to be emptied,
259
+ * or Pi answers words the user believes they took back.
260
+ */
261
+ clearQueues?(conversationId: string): Promise<{
262
+ steering: string[];
263
+ followUp: string[];
264
+ }>;
239
265
  /**
240
266
  * ONE-SHOT TITLE GENERATION. Spawns its own short-lived Pi child rather than borrowing a running one.
241
267
  *
@@ -966,6 +992,31 @@ export declare class PiRpcRuntime implements AgentRuntime {
966
992
  * silent success.
967
993
  */
968
994
  private sessionFor;
995
+ /**
996
+ * Hands a message to Pi's FOLLOW-UP queue, to be answered when the current run finishes its work.
997
+ *
998
+ * WHY THIS EXISTS ALONGSIDE A STEER. Both put a message into a queue Pi drains inside the running
999
+ * turn, and neither of them re-runs `_prepareRun` - so neither re-emits `before_agent_start`, and
1000
+ * neither pays for the memory search and the system-prompt patch that hook does. A queued message
1001
+ * that answered itself with a fresh `prompt()` paid for both, once per message, and the prompt it
1002
+ * sent was larger than the one before it.
1003
+ *
1004
+ * The difference between the two is WHEN Pi delivers the message: a steer lands at the next turn
1005
+ * boundary (in the middle of the answer, which is what 「插队」 means), and a follow-up lands when
1006
+ * the agent would otherwise stop (after the answer, which is what a message typed while the agent
1007
+ * was busy means).
1008
+ */
1009
+ followUp(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
1010
+ /**
1011
+ * Empties Pi's steering and follow-up queues and returns what was in them.
1012
+ *
1013
+ * The session path asks Pi directly (`session.clearQueue`). The one-shot path writes Pi's
1014
+ * `clear_queue` command, which answers with the text it removed.
1015
+ */
1016
+ clearQueues(conversationId: string): Promise<{
1017
+ steering: string[];
1018
+ followUp: string[];
1019
+ }>;
969
1020
  steer(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
970
1021
  /**
971
1022
  * The model/thinking catalog, from Pi, cached.
@@ -2097,6 +2097,76 @@ export class PiRpcRuntime {
2097
2097
  throw new SteerUnavailableError(`会话 ${conversationId} 还没有可用的 Pi 会话`);
2098
2098
  return session;
2099
2099
  }
2100
+ /**
2101
+ * Hands a message to Pi's FOLLOW-UP queue, to be answered when the current run finishes its work.
2102
+ *
2103
+ * WHY THIS EXISTS ALONGSIDE A STEER. Both put a message into a queue Pi drains inside the running
2104
+ * turn, and neither of them re-runs `_prepareRun` - so neither re-emits `before_agent_start`, and
2105
+ * neither pays for the memory search and the system-prompt patch that hook does. A queued message
2106
+ * that answered itself with a fresh `prompt()` paid for both, once per message, and the prompt it
2107
+ * sent was larger than the one before it.
2108
+ *
2109
+ * The difference between the two is WHEN Pi delivers the message: a steer lands at the next turn
2110
+ * boundary (in the middle of the answer, which is what 「插队」 means), and a follow-up lands when
2111
+ * the agent would otherwise stop (after the answer, which is what a message typed while the agent
2112
+ * was busy means).
2113
+ */
2114
+ async followUp(conversationId, message, images) {
2115
+ // THE SESSION PATH. `session.followUp` is Pi's own follow-up queue, so the message joins the run
2116
+ // that is already going and is delivered by the agent loop when that run has nothing left to do.
2117
+ if (this.activeTurns.has(conversationId)) {
2118
+ const session = await this.sessionFor(conversationId);
2119
+ const imagePayloads = this.options.uploadsDir && images?.length
2120
+ ? await loadConversationImages(this.options.uploadsDir, conversationId, images)
2121
+ : undefined;
2122
+ if (!session.followUp)
2123
+ throw new SteerUnavailableError(`会话 ${conversationId} 的 Pi 不支持排队消息`);
2124
+ const disposition = await session.followUp(message, imagePayloads);
2125
+ return disposition === "handled" ? "handled" : "queued";
2126
+ }
2127
+ // NO TURN TO FOLLOW. A follow-up is only meaningful while something is running - with nothing
2128
+ // running, Pi would hold the message until the NEXT run, which is the same answer arriving much
2129
+ // later than a prompt would have given it.
2130
+ //
2131
+ // THE ONE-SHOT PATH IS THIS CASE TOO, and it is not an accident: that path runs each turn in a
2132
+ // short-lived child that is killed when the turn ends, so a follow-up queue would die with it. The
2133
+ // message falls back to being answered by a turn of its own, which is the only thing that path can
2134
+ // do anyway.
2135
+ throw new SteerUnavailableError(`会话 ${conversationId} 当前没有可以接手的 Pi 会话,无法排队`);
2136
+ }
2137
+ /**
2138
+ * Empties Pi's steering and follow-up queues and returns what was in them.
2139
+ *
2140
+ * The session path asks Pi directly (`session.clearQueue`). The one-shot path writes Pi's
2141
+ * `clear_queue` command, which answers with the text it removed.
2142
+ */
2143
+ async clearQueues(conversationId) {
2144
+ const empty = { steering: [], followUp: [] };
2145
+ if (this.activeTurns.has(conversationId)) {
2146
+ const session = this.sessionPool.get(conversationId);
2147
+ if (!session?.clearQueue)
2148
+ return empty;
2149
+ try {
2150
+ const cleared = session.clearQueue();
2151
+ return { steering: [...(cleared?.steering ?? [])], followUp: [...(cleared?.followUp ?? [])] };
2152
+ }
2153
+ catch (error) {
2154
+ console.warn(`PiRpcRuntime: clearing the queues of ${conversationId} threw: ${error instanceof Error ? error.message : "unknown error"}`);
2155
+ return empty;
2156
+ }
2157
+ }
2158
+ const run = this.runs.get(conversationId);
2159
+ if (!run || !this.canSteer(conversationId))
2160
+ return empty;
2161
+ // THE ONE-SHOT PATH NEEDS NO CLEAR, and the reason is structural rather than convenient: that path
2162
+ // runs each turn in a short-lived child that is KILLED when the turn ends (`answerOneShot`'s
2163
+ // finally), and the child's queues die with it. A message Pi was holding in that child is gone the
2164
+ // moment the turn is over, so there is nothing to clear and nothing to wait for. Clearing it
2165
+ // anyway would cost a round trip to a child that is about to be killed, on the path between a turn
2166
+ // ending and the next one starting.
2167
+ void run;
2168
+ return empty;
2169
+ }
2100
2170
  async steer(conversationId, message, images) {
2101
2171
  // THE SESSION PATH. A steer on a persistent session is a `prompt` with `streamingBehavior:
2102
2172
  // "steer"` - the same queueing semantics Pi's RPC `steer` command has, expressed the way the SDK
@@ -545,8 +545,14 @@ export declare class Store {
545
545
  * was written into the transcript when the user pressed send, long before Pi ever saw it - and it stays
546
546
  * exactly where it was, now with the agent answering it. Deleting it is what made a steered reply appear
547
547
  * with no question above it.
548
+ *
549
+ * `messageId` says WHICH message Pi picked up, for the case where position cannot answer it. A message
550
+ * handed to Pi's follow-up queue was never CLAIMED here - it is still `waiting`, because Super Pi keeps
551
+ * it in its own list so the user can still withdraw it - and a conversation can hold a `waiting` message
552
+ * and a `sending` one at the same time. The caller resolves the delivery by the text Pi announced,
553
+ * which is the same text Pi itself matched on.
548
554
  */
549
- deliverWaitingMessage(conversationId: string, agentIdentity?: Message["agentIdentity"]): Promise<{
555
+ deliverWaitingMessage(conversationId: string, agentIdentity?: Message["agentIdentity"], messageId?: string): Promise<{
550
556
  user: Message;
551
557
  assistant: Message;
552
558
  } | undefined>;
@@ -1634,17 +1634,27 @@ export class Store {
1634
1634
  * was written into the transcript when the user pressed send, long before Pi ever saw it - and it stays
1635
1635
  * exactly where it was, now with the agent answering it. Deleting it is what made a steered reply appear
1636
1636
  * with no question above it.
1637
+ *
1638
+ * `messageId` says WHICH message Pi picked up, for the case where position cannot answer it. A message
1639
+ * handed to Pi's follow-up queue was never CLAIMED here - it is still `waiting`, because Super Pi keeps
1640
+ * it in its own list so the user can still withdraw it - and a conversation can hold a `waiting` message
1641
+ * and a `sending` one at the same time. The caller resolves the delivery by the text Pi announced,
1642
+ * which is the same text Pi itself matched on.
1637
1643
  */
1638
- async deliverWaitingMessage(conversationId, agentIdentity) {
1644
+ async deliverWaitingMessage(conversationId, agentIdentity, messageId) {
1639
1645
  const conversation = this.requireConversation(conversationId);
1640
1646
  // Looks for the single message Pi is currently answering. A `role: "tool"` 后台回传 that has been
1641
1647
  // steered or taken is on the same delivery state machine as a user message, so it is the answer
1642
1648
  // target too; the assistant placeholder still lands directly below it and the role/identity is read
1643
1649
  // from the conversation (not the message) when the turn opens.
1644
- const index = conversation.messages.findIndex((item) => (item.role === "user" || item.role === "tool") && item.delivery === "sending");
1650
+ const index = messageId
1651
+ ? conversation.messages.findIndex((item) => item.id === messageId)
1652
+ : conversation.messages.findIndex((item) => (item.role === "user" || item.role === "tool") && item.delivery === "sending");
1645
1653
  if (index < 0)
1646
1654
  return undefined;
1647
1655
  const user = conversation.messages[index];
1656
+ if (user.role !== "user" && user.role !== "tool")
1657
+ return undefined;
1648
1658
  delete user.delivery;
1649
1659
  const assistant = { id: id("message"), role: "assistant", content: "", createdAt: now(), ...(agentIdentity ? { agentIdentity: structuredClone(agentIdentity) } : {}) };
1650
1660
  conversation.messages.splice(index + 1, 0, assistant);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -75,6 +75,7 @@
75
75
  "scripts/agent-vis-records.test.mjs",
76
76
  "scripts/queue-store.test.mjs",
77
77
  "scripts/followup.test.mjs",
78
+ "scripts/pi-followup-queue.test.mjs",
78
79
  "scripts/md-render.test.mjs",
79
80
  "scripts/revert-session-format.test.mjs",
80
81
  "scripts/revert-session-file.test.mjs",
@@ -21,6 +21,7 @@ import { mkdtempSync, rmSync, existsSync, statSync, readFileSync } from "node:fs
21
21
  import { homedir, tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
+ import { createRequire } from "node:module";
24
25
 
25
26
  /** The Node version Pi requires. Below it, module loading and jiti transpiling are measurably slower. */
26
27
  const PI_NODE_REQUIREMENT = "22.19.0";
@@ -50,23 +51,42 @@ function atLeast(actual, required) {
50
51
  * and a message used to cost a full write of all of it before the browser was told the turn had
51
52
  * started. The size of that state is invisible from a fresh install and decisive on a real one, so it
52
53
  * is reported from the REAL data directory, read-only, and the write is measured on a COPY.
54
+ *
55
+ * THE NUMBER THAT MATTERS IS THE STATE, NOT THE FILE. An earlier version of this report called the
56
+ * SQLite FILE size "the state", and that is a different number: the file holds the base snapshot AND
57
+ * the incremental overlay tables, and the overlay is never pruned, so the file grows with every write
58
+ * while the state the BROWSER RECEIVES does not grow the same way. The number that costs time is the
59
+ * one that is cloned (`store.snapshot()`), serialised (`JSON.stringify`) and parsed (`JSON.parse` in
60
+ * the browser) on every state broadcast - so that is what is measured here, by rebuilding the state
61
+ * out of the database and measuring its JSON.
53
62
  */
54
63
  function measureRealState(dataDir) {
55
- const result = { path: "", bytes: 0, conversations: 0, messages: 0, fullWriteMs: undefined };
64
+ const result = { path: "", bytes: 0, fileBytes: 0, conversations: 0, messages: 0, fullWriteMs: undefined };
56
65
  for (const name of ["state.sqlite", "state.json"]) {
57
66
  const path = join(dataDir, name);
58
67
  if (!existsSync(path)) continue;
59
68
  result.path = path;
60
69
  try {
61
- result.bytes = statSync(path).size;
70
+ result.fileBytes = statSync(path).size;
62
71
  if (name === "state.json") {
63
72
  const parsed = JSON.parse(readFileSync(path, "utf8"));
64
73
  result.conversations = parsed.conversations?.length ?? 0;
65
74
  result.messages = (parsed.conversations ?? []).reduce((sum, c) => sum + (c.messages?.length ?? 0), 0);
75
+ // THE SIZE THAT IS ACTUALLY SENT. The browser gets this JSON, not the file.
76
+ result.bytes = Buffer.byteLength(readFileSync(path, "utf8"), "utf8");
66
77
  } else {
67
- // The SQLite file holds the base snapshot AND the incremental overlays, so its size is the
68
- // closest cheap proxy for how much state there is.
69
- result.bytes = statSync(path).size;
78
+ // THE SQLITE STATE, REBUILT AND MEASURED. The repository's own loader is the authority on what
79
+ // the state IS, so the report does not guess at it: the database is opened READ-ONLY, the
80
+ // overlays are folded onto the base snapshot the way `load()` folds them, and the result is
81
+ // measured as JSON - the same bytes a state broadcast carries.
82
+ const measured = measureSqliteState(path);
83
+ if (measured.bytes > 0) {
84
+ result.bytes = measured.bytes;
85
+ result.conversations = measured.conversations;
86
+ result.messages = measured.messages;
87
+ } else {
88
+ result.bytes = result.fileBytes;
89
+ }
70
90
  }
71
91
  } catch { /* unreadable is not a fact worth reporting */ }
72
92
  break;
@@ -74,6 +94,72 @@ function measureRealState(dataDir) {
74
94
  return result;
75
95
  }
76
96
 
97
+ /**
98
+ * The size of the state a SQLite store holds, measured as the JSON a broadcast would carry.
99
+ *
100
+ * READ-ONLY, and it opens its OWN connection rather than the server's: a report must not disturb the
101
+ * session someone is in the middle of, and a second read-only connection sees a consistent snapshot
102
+ * without taking any lock the server needs.
103
+ */
104
+ function measureSqliteState(path) {
105
+ let db;
106
+ const empty = { bytes: 0, conversations: 0, messages: 0 };
107
+ try {
108
+ const require = createRequire(import.meta.url);
109
+ const { DatabaseSync } = require("node:sqlite");
110
+ db = new DatabaseSync(path, { readOnly: true });
111
+ const row = db.prepare("SELECT json FROM state WHERE id=1").get();
112
+ if (!row) return empty;
113
+ const state = JSON.parse(String(row.json));
114
+ if (!Array.isArray(state.conversations)) state.conversations = [];
115
+ // THE OVERLAYS, folded the way `load()` folds them: a conversation's metadata replaces its fields
116
+ // except its messages, and its messages are the union of the base array and the overlay rows - with
117
+ // the authoritative order rows winning when they are there.
118
+ const metadata = db.prepare("SELECT conversation,json FROM metadata").all();
119
+ const overrides = db.prepare("SELECT conversation,id,json FROM messages").all();
120
+ const ordered = db.prepare("SELECT conversation,id,position,json FROM message_order").all();
121
+ const byConversation = new Map();
122
+ for (const item of overrides) {
123
+ const list = byConversation.get(String(item.conversation)) ?? new Map();
124
+ list.set(String(item.id), JSON.parse(String(item.json)));
125
+ byConversation.set(String(item.conversation), list);
126
+ }
127
+ const orderByConversation = new Map();
128
+ for (const item of ordered) {
129
+ const list = orderByConversation.get(String(item.conversation)) ?? [];
130
+ list.push({ position: Number(item.position), message: JSON.parse(String(item.json)) });
131
+ orderByConversation.set(String(item.conversation), list);
132
+ }
133
+ for (const item of metadata) {
134
+ const conversation = state.conversations.find((entry) => entry.id === String(item.conversation));
135
+ if (!conversation) continue;
136
+ const messages = conversation.messages;
137
+ for (const key of Object.keys(conversation)) Reflect.deleteProperty(conversation, key);
138
+ Object.assign(conversation, JSON.parse(String(item.json)), { messages });
139
+ }
140
+ for (const conversation of state.conversations) {
141
+ const map = byConversation.get(conversation.id) ?? new Map();
142
+ const order = orderByConversation.get(conversation.id) ?? [];
143
+ if (order.length) {
144
+ conversation.messages = order.sort((a, b) => a.position - b.position).map((entry) => entry.message);
145
+ } else if (Array.isArray(conversation.messages)) {
146
+ conversation.messages = conversation.messages.map((message) => map.get(message.id) ?? message);
147
+ } else {
148
+ conversation.messages = [];
149
+ }
150
+ }
151
+ return {
152
+ bytes: Buffer.byteLength(JSON.stringify(state), "utf8"),
153
+ conversations: state.conversations.length,
154
+ messages: state.conversations.reduce((sum, conversation) => sum + (conversation.messages?.length ?? 0), 0),
155
+ };
156
+ } catch {
157
+ return empty;
158
+ } finally {
159
+ try { db?.close(); } catch { /* already closed */ }
160
+ }
161
+ }
162
+
77
163
  /**
78
164
  * Runs the diagnosis against a throwaway server and returns what it measured.
79
165
  *
@@ -276,6 +362,14 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
276
362
  } catch { /* reported as not measured below */ }
277
363
  const interruptText = Date.now() - clicked;
278
364
  const interruptLog = log.slice(interruptLogMark);
365
+ // The whole trace of the interrupt, in order. It is kept because the interrupt is the one path
366
+ // whose cost has turned out to be surprising, and a number without the phases around it cannot
367
+ // say which phase the number belongs to.
368
+ result.interruptTrace = interruptLog
369
+ .split("\n")
370
+ .filter((line) => line.includes("[super-pi latency]"))
371
+ .map((line) => line.replace(/.*\[super-pi latency\] /, "").trim())
372
+ .slice(0, 24);
279
373
  result.interrupt = {
280
374
  toWaitingMs: interruptWaiting || undefined,
281
375
  toTextMs: interruptText,
@@ -330,6 +424,11 @@ export function renderReport(result) {
330
424
  lines.push(` 第${turn.turn}轮 等待中 -> 出字 ${ms(turn.modelMs)} 模型 (不是 Super Pi)`);
331
425
  }
332
426
  if (result.interrupt) {
427
+ if (Array.isArray(result.interruptTrace) && result.interruptTrace.length) {
428
+ lines.push(" 打断时的完整阶段(按顺序):");
429
+ for (const entry of result.interruptTrace) lines.push(` ${entry}`);
430
+ }
431
+
333
432
  lines.push("");
334
433
  lines.push(" 打断(点「打断并回答」之后):");
335
434
  lines.push(` 点击 -> 新一轮报等待中 ${ms(result.interrupt.toWaitingMs)} Super Pi (等旧轮退完 + 等槽位)`);
@@ -366,10 +465,16 @@ export function renderReport(result) {
366
465
  lines.push(` ${real.path}`);
367
466
  const megabytes = (real.bytes / 1024 / 1024).toFixed(2);
368
467
  const shape = real.conversations ? `,${real.conversations} 个会话,${real.messages} 条消息` : "";
369
- lines.push(` 大小 ${megabytes}MB${shape}`);
468
+ lines.push(` 浏览器每次收到的那份 state:${megabytes}MB${shape}`);
469
+ if (real.fileBytes && real.fileBytes !== real.bytes) {
470
+ lines.push(` 数据库文件本身:${(real.fileBytes / 1024 / 1024).toFixed(2)}MB`);
471
+ lines.push(" (文件比 state 大是因为它还存着增量写的那几层,那些层从不被清理;");
472
+ lines.push(" 真正花时间的是上面那个数字——每次广播都要克隆它、序列化它、");
473
+ lines.push(" 再在浏览器里解析一遍。)");
474
+ }
370
475
  if (real.bytes > 1024 * 1024) {
371
- lines.push(" 这个体积的 state 如果每发一条消息都要全量写一遍,在慢机器上就是秒级——");
372
- lines.push(" 而那次写就挡在「服务端告诉浏览器回合开始了」之前。");
476
+ lines.push(" 这个体积的 state 每变一次就要整份克隆 + 整份序列化 + 浏览器整份解析,");
477
+ lines.push(" 在慢机器上每一次广播都是秒级,而一轮里会广播好几次。");
373
478
  }
374
479
  }
375
480