@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.
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +3 -3
- package/dist/client/assets/{index-pD4FaDWr.js → index-mAu39Bra.js} +2 -2
- package/dist/client/index.html +1 -1
- package/dist/server/application/run-service.d.ts +51 -0
- package/dist/server/application/run-service.js +157 -7
- package/dist/server/pi-session.d.ts +25 -0
- package/dist/server/runtime.d.ts +51 -0
- package/dist/server/runtime.js +70 -0
- package/dist/server/store.d.ts +7 -1
- package/dist/server/store.js +12 -2
- package/package.json +2 -1
- package/scripts/diagnose.mjs +113 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "b8deff533f84ef04833217897a1670a38ace531494f44b488da818198f0e74cb",
|
|
3
3
|
"files": {
|
|
4
4
|
"src/client/App.tsx": "f03962cde47d8c98ec59dad6ea365ee140d7768157b5894cbc6a4d377648bae7",
|
|
5
|
-
"src/client/FilesPanel.tsx": "
|
|
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-
|
|
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
|
---
|
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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. */
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -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.
|
package/dist/server/runtime.js
CHANGED
|
@@ -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
|
package/dist/server/store.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/store.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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",
|
package/scripts/diagnose.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
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(`
|
|
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
|
|