zen-gitsync 2.17.21 → 2.17.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +20 -12
  2. package/package.json +10 -1
  3. package/scripts/release.js +214 -82
  4. package/src/ui/public/assets/{AgentEngineSelector-D_8VedA0.js → AgentEngineSelector-DLJ8He83.js} +1 -1
  5. package/src/ui/public/assets/{AgentView-CpKIfQLq.js → AgentView-CQ18nw02.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-kyk4Mmvf.js → AppVersionBadge-1pyUzRer.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BUPiKcdW.js → BranchSelector-DrsrZiWJ.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-CMe1gjY-.js → CommitForm-B5p-7P7m.js} +1 -1
  9. package/src/ui/public/assets/{CommonDialog-DMWtstZq.js → CommonDialog-BEm2cK1X.js} +1 -1
  10. package/src/ui/public/assets/{EditorView-mUmFBb7H.css → EditorView-C6HFYeEM.css} +1 -1
  11. package/src/ui/public/assets/EditorView-fuMrS9P7.js +1 -0
  12. package/src/ui/public/assets/{FileDiffViewer-CvN5b1ZC.js → FileDiffViewer-CYxcLw10.js} +1 -1
  13. package/src/ui/public/assets/{FlowExecutionViewer-Ynr4q8qE.js → FlowExecutionViewer-DTmlBLXQ.js} +1 -1
  14. package/src/ui/public/assets/{FlowOrchestrationWorkspace-DPDZc9uf.js → FlowOrchestrationWorkspace-Da_UmTRT.js} +1 -1
  15. package/src/ui/public/assets/{IconButton-CFJkDLVG.js → IconButton-BShiC2AP.js} +1 -1
  16. package/src/ui/public/assets/{LogList-CbfsCVsy.js → LogList-5M5JBhlB.js} +1 -1
  17. package/src/ui/public/assets/{MindmapView-D8A1V-Fd.js → MindmapView-BUV_w14M.js} +1 -1
  18. package/src/ui/public/assets/{MonitorView-Bm_vUP8Y.js → MonitorView-D9N4QH5Y.js} +1 -1
  19. package/src/ui/public/assets/{ProjectStartupButton-BBND5XcZ.js → ProjectStartupButton-DYYQ-O7P.js} +1 -1
  20. package/src/ui/public/assets/RecentDirectoriesChat-BtWa_HMY.js +1 -0
  21. package/src/ui/public/assets/RecentDirectoriesChat-vHT8jMxW.css +1 -0
  22. package/src/ui/public/assets/{RemoteManagerDialog-BVQeIr8U.js → RemoteManagerDialog-C4QIJK_F.js} +1 -1
  23. package/src/ui/public/assets/{RemoteRepoCard-vuCu_cpA.js → RemoteRepoCard-BmDh538v.js} +1 -1
  24. package/src/ui/public/assets/{SourceMapView-BZjgxQM8.js → SourceMapView-Ceamya-N.js} +1 -1
  25. package/src/ui/public/assets/{SvgIcon-D0hK-p1U.js → SvgIcon-xtz4vvc9.js} +1 -1
  26. package/src/ui/public/assets/{TemplateManager-XoPZv_i6.js → TemplateManager-DhKTZ4EM.js} +1 -1
  27. package/src/ui/public/assets/{UserInputNode-CZarmsrv.js → UserInputNode-DfeCGlEO.js} +1 -1
  28. package/src/ui/public/assets/WorkbenchView-D9l3i4Qj.css +1 -0
  29. package/src/ui/public/assets/WorkbenchView-DX2epPD9.js +6 -0
  30. package/src/ui/public/assets/{_plugin-vue_export-helper-N8Vc5eEs.js → _plugin-vue_export-helper-C_bQVzeb.js} +4 -4
  31. package/src/ui/public/assets/{agentConversations-Da7zLPfP.js → agentConversations-BUPj8vzH.js} +1 -1
  32. package/src/ui/public/assets/configStore-DJH7OlB_.js +1 -0
  33. package/src/ui/public/assets/{gitStore-grKwgyHH.js → gitStore-ByP9lpuT.js} +1 -1
  34. package/src/ui/public/assets/index-CFX8sK4w.js +29 -0
  35. package/src/ui/public/assets/{previewSandbox-D4455rKO.js → previewSandbox-DSzZ_Rky.js} +1 -1
  36. package/src/ui/public/index.html +10 -10
  37. package/src/ui/public/sounds/CREDITS.txt +32 -0
  38. package/src/ui/public/sounds/task-done.wav +0 -0
  39. package/src/ui/public/sounds/task-error.wav +0 -0
  40. package/src/ui/server/routes/config.js +4 -1
  41. package/src/ui/server/routes/workbench/index.js +15 -6
  42. package/src/ui/server/routes/workbench/jobActivity.js +274 -0
  43. package/src/ui/server/routes/workbench/jobActivity.test.js +295 -0
  44. package/src/ui/server/routes/workbench/liveJobs.js +3 -0
  45. package/src/ui/server/routes/workbench/orchestratorStore.js +53 -2
  46. package/src/ui/server/routes/workbench/orchestratorStore.progressReport.test.js +177 -0
  47. package/src/ui/server/routes/workbench/progressReport.js +54 -47
  48. package/src/ui/server/routes/workbench/progressReport.test.js +73 -42
  49. package/src/ui/server/routes/workbench/projectRegistry.js +36 -2
  50. package/src/ui/server/routes/workbench/projectRegistry.test.js +98 -0
  51. package/src/ui/server/routes/workbench/taskRunner.js +9 -0
  52. package/src/utils/selfUpdatePolicy.js +123 -0
  53. package/src/utils/selfUpdatePolicy.test.js +204 -0
  54. package/src/ui/public/assets/EditorView-C6msf9YP.js +0 -1
  55. package/src/ui/public/assets/RecentDirectoriesChat-BQbncY1L.css +0 -1
  56. package/src/ui/public/assets/RecentDirectoriesChat-D9fMFh8r.js +0 -1
  57. package/src/ui/public/assets/WorkbenchView-BLyXsdYw.css +0 -1
  58. package/src/ui/public/assets/WorkbenchView-CPur8WEj.js +0 -6
  59. package/src/ui/public/assets/configStore-BFb0I8eP.js +0 -1
  60. package/src/ui/public/assets/index-BHYAwndE.js +0 -29
@@ -1,5 +1,5 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/office-docx-B0sJQnr-.js","assets/rolldown-runtime-C0FnF6B9.js","assets/element-plus-Bx3j6vzG.js","assets/element-plus-Qexo5hiM.css","assets/office-excel-DMRySOdK.js","assets/office-pptx-Do2blxao.js"])))=>i.map(i=>d[i]);
2
- import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{Bt as t,Cn as n,Dn as r,En as i,Er as a,H as o,Jn as s,K as c,Kn as l,Mn as u,Nn as d,Pn as f,Pr as p,Qn as m,Qr as h,Rn as g,St as _,Tn as v,Xn as y,Yr as b,an as x,br as S,cr as C,on as w,or as T,pr as E,qr as D,tr as O,wn as k,yn as A}from"./element-plus-Bx3j6vzG.js";import{p as j}from"./vendor-CyWsJd6S.js";import{t as M}from"./_plugin-vue_export-helper-N8Vc5eEs.js";import{i as N}from"./monaco-DkJuNcCr.js";import{n as P,r as F}from"./flow-mindmap-OPE_D4da.js";var I=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`ico`,`svg`]);function L(e){if(!e)return!1;let t=e.split(`.`).pop()?.toLowerCase()??``;return I.has(t)}function ee(e){return`/api/editor/raw?path=${encodeURIComponent(e)}`}var R=new Set([`doc`,`docx`,`xls`,`xlsx`,`ppt`,`pptx`,`odt`,`ods`,`odp`]);function z(e){return e.split(/[\\/]/).pop()?.split(`.`).pop()?.toLowerCase()||``}function B(e){return R.has(z(e))}function V(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/preview?${n.toString()}`}function H(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/raw?${n.toString()}`}g(),p(),E();var te={class:`image-preview-toolbar`},U=[`title`],W={key:0,class:`image-preview-dim`},G={key:1,class:`image-preview-dim`},K={key:0,class:`image-preview-error`},q={class:`image-preview-error-path`},J=[`src`,`alt`],Y={class:`image-preview-footer`},X={class:`image-preview-zoom`},Z=M(f({__name:`ImagePreview`,props:{filePath:{},fileName:{}},setup(e){let i=e,d=S(null),f=S(1),p=S(0),m=S(0),g=S(!1),v=S({x:0,y:0}),E=S(null),O=S(!1),A=S(null),j=()=>{if(!d.value||!E.value)return;let e=d.value.clientWidth-32,t=d.value.clientHeight-32;if(e<=0||t<=0)return;let n=Math.min(e/E.value.w,t/E.value.h,1);f.value=n,p.value=0,m.value=0},M=()=>{f.value=Math.min(f.value*1.2,8)},N=()=>{f.value=Math.max(f.value/1.2,.1)},P=()=>{f.value=1,p.value=0,m.value=0,j()},F=e=>{e.preventDefault(),e.deltaY<0?M():N()},I=e=>{e.button===0&&(g.value=!0,v.value={x:e.clientX-p.value,y:e.clientY-m.value})},L=e=>{g.value&&(p.value=e.clientX-v.value.x,m.value=e.clientY-v.value.y)},R=()=>{g.value=!1},z=e=>{let t=e.target;E.value={w:t.naturalWidth,h:t.naturalHeight},O.value=!1,requestAnimationFrame(()=>j())},B=()=>{O.value=!0},V=n(()=>i.fileName||i.filePath?.split(`/`).pop()||``),H=n(()=>ee(i.filePath));return s(()=>{window.addEventListener(`mousemove`,L),window.addEventListener(`mouseup`,R),window.addEventListener(`resize`,j)}),l(()=>{window.removeEventListener(`mousemove`,L),window.removeEventListener(`mouseup`,R),window.removeEventListener(`resize`,j)}),T(()=>i.filePath,()=>{E.value=null,O.value=!1,f.value=1,p.value=0,m.value=0}),(e,n)=>{let s=o;return y(),r(`div`,{class:`image-preview`,ref_key:`containerRef`,ref:d,onWheel:F},[k(`div`,te,[k(`span`,{class:`image-preview-name`,title:i.filePath},h(V.value),9,U),E.value?(y(),r(`span`,W,h(E.value.w)+` × `+h(E.value.h)+` px`,1)):(y(),r(`span`,G,h(Math.round(f.value*100))+`%`,1)),n[0]||=k(`div`,{class:`image-preview-spacer`},null,-1),u(s,{content:`放大`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:M},[u(a(c),null,{default:C(()=>[u(a(x))]),_:1})])]),_:1}),u(s,{content:`缩小`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:N},[u(a(c),null,{default:C(()=>[u(a(w))]),_:1})])]),_:1}),u(s,{content:`适应窗口`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:P},[u(a(c),null,{default:C(()=>[u(a(t))]),_:1})])]),_:1})]),k(`div`,{class:D([`image-preview-stage`,{"is-dragging":g.value}]),onMousedown:I},[O.value?(y(),r(`div`,K,[u(a(c),{size:`32`,color:`var(--color-danger)`},{default:C(()=>[u(a(_))]),_:1}),n[1]||=k(`p`,null,`图片加载失败`,-1),k(`p`,q,h(i.filePath),1)])):(y(),r(`img`,{key:1,ref_key:`imgRef`,ref:A,src:H.value,alt:V.value,class:`image-preview-img`,style:b({transform:`translate(${p.value}px, ${m.value}px) scale(${f.value})`}),onLoad:z,onError:B,draggable:`false`},null,44,J))],34),k(`div`,Y,[n[2]||=k(`span`,null,`滚轮缩放 · 拖拽平移 · 双击重置`,-1),k(`span`,X,h(Math.round(f.value*100))+`%`,1)])],544)}}}),[[`__scopeId`,`data-v-28b3ff72`]]);g(),E();var ne={class:`md-preview`},re=[`innerHTML`],ie={key:1,class:`md-mindmap`},ae=M(f({__name:`MarkdownPreview`,props:{content:{},allowHtml:{type:Boolean,default:!0}},setup(e){let t=e;function i(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);return t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, form, input, button, textarea, select`).forEach(e=>e.remove()),t.content.querySelectorAll(`*`).forEach(e=>{for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase();if(n.startsWith(`on`)||n===`style`||n===`srcdoc`){e.removeAttribute(t.name);continue}if(n===`href`||n===`src`||n===`xlink:href`){let r=t.value.trim().replace(/[\u0000-\u0020]+/g,``);/^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i.test(r)||n===`src`&&/^data:image\/(?:png|gif|jpe?g|webp);base64,/i.test(r)||e.removeAttribute(t.name)}}e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`)}),t.innerHTML}let o=n(()=>{let e=t.content||``,n=[],r=`@@FLOWDASH_MINDMAP_BLOCK_`,a=e.replace(/```[ \t]*mindmap[ \t]*\n([\s\S]*?)```/gi,(e,t)=>{let i=n.length;return n.push({id:i,md:t}),`${r}${i}@@`}),o=j(a,{className:!1,html:t.allowHtml}),s=t.allowHtml?o:i(o),c=RegExp(`(?:<p>)?\\s*${r}(\\d+)@@\\s*(?:</p>)?`,`g`),l=[],u=0,d;for(;(d=c.exec(s))!==null;){d.index>u&&l.push({type:`html`,html:s.slice(u,d.index)});let e=Number(d[1]);l.push({type:`mindmap`,id:e,md:n[e].md,data:P(n[e].md)}),u=d.index+d[0].length}return u<s.length&&l.push({type:`html`,html:s.slice(u)}),l.length===0&&l.push({type:`html`,html:`<p class="md-empty">无内容</p>`}),l});return(e,t)=>(y(),r(`div`,ne,[(y(!0),r(A,null,m(o.value,(e,n)=>(y(),r(A,{key:n},[e.type===`html`?(y(),r(`div`,{key:0,class:`md-segment`,innerHTML:e.html},null,8,re)):(y(),r(`div`,ie,[t[0]||=k(`div`,{class:`md-mindmap-title`},`四维导图`,-1),u(a(F),{data:e.data,"preview-mode":``,class:`md-mindmap-canvas`},null,8,[`data`])]))],64))),128))]))}}),[[`__scopeId`,`data-v-9514c256`]]);g(),p(),E();var oe={class:`office-preview`},se={class:`office-preview-toolbar`},ce={class:`office-preview-badge`},le={key:0,class:`office-preview-side`},ue={key:0,class:`office-preview-state`},Q={key:1,class:`office-preview-state office-preview-error`},de=[`src`],fe=M(f({__name:`OfficePreview`,props:{filePath:{},source:{default:`worktree`},rev:{default:``},side:{default:`new`}},setup(t){let a=t,o=d(()=>N(()=>import(`./office-docx-B0sJQnr-.js`).then(t=>e(t.t(),1)),__vite__mapDeps([0,1,2,3]))),s=d(()=>N(()=>import(`./office-excel-DMRySOdK.js`).then(t=>e(t.t(),1)),__vite__mapDeps([4,1,0,2,3]))),c=d(()=>N(()=>import(`./office-pptx-Do2blxao.js`).then(t=>e(t.t(),1)),__vite__mapDeps([5,1,0,2,3]))),u=n(()=>z(a.filePath)),f=n(()=>u.value===`docx`?o:u.value===`xls`||u.value===`xlsx`?s:u.value===`pptx`?c:null),p=n(()=>H(a.filePath,{source:a.source,rev:a.rev,side:a.side})),m=n(()=>V(a.filePath,{source:a.source,rev:a.rev,side:a.side})),g=S(null),_=S(``),b=S(!1),x=S(!1),C=S(!1),w=0;function E(){_.value&&URL.revokeObjectURL(_.value),_.value=``}async function D(e){try{let t=await fetch(m.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;_.value=URL.createObjectURL(await t.blob()),C.value=!0,x.value=!1}catch{e===w&&(x.value=!0)}}async function A(){let e=++w;if(b.value=!0,x.value=!1,C.value=!1,g.value=null,E(),!f.value){await D(e),e===w&&(b.value=!1);return}try{let t=await fetch(p.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;g.value=await t.arrayBuffer()}catch{await D(e)}finally{e===w&&(b.value=!1)}}function j(){if(!C.value){let e=w;g.value=null,b.value=!0,D(e).finally(()=>{e===w&&(b.value=!1)})}}function M(){A()}return T(p,()=>{A()},{immediate:!0}),l(()=>{w++,E()}),(e,n)=>(y(),r(`div`,oe,[k(`div`,se,[n[0]||=k(`span`,{class:`office-preview-title`},`Office 预览`,-1),k(`span`,ce,h(u.value.toUpperCase()),1),t.source===`git`?(y(),r(`span`,le,h(t.side===`old`?`旧版本`:`新版本`),1)):i(``,!0),k(`button`,{class:`office-preview-reload`,type:`button`,onClick:M},`刷新`)]),b.value?(y(),r(`div`,ue,`正在生成预览…`)):x.value?(y(),r(`div`,Q,[...n[1]||=[k(`p`,null,`无法预览此 Office 文件。`,-1),k(`p`,{class:`office-preview-hint`},`支持 docx、xls/xlsx、pptx;老格式需要安装 LibreOffice 才能兜底预览。`,-1)]])):C.value?(y(),r(`iframe`,{key:2,class:`office-preview-frame`,src:_.value,title:`Office 文件 PDF 预览`},null,8,de)):f.value&&g.value?(y(),v(O(f.value),{key:3,class:`office-preview-document`,src:g.value,onError:j},null,40,[`src`])):i(``,!0)]))}}),[[`__scopeId`,`data-v-89cd4207`]]),pe=`allow-scripts allow-forms allow-popups allow-modals`,$=`<script>(function(){
2
+ import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{Bt as t,Cn as n,Dn as r,En as i,Er as a,H as o,Jn as s,K as c,Kn as l,Mn as u,Nn as d,Pn as f,Pr as p,Qn as m,Qr as h,Rn as g,St as _,Tn as v,Xn as y,Yr as b,an as x,br as S,cr as C,on as w,or as T,pr as E,qr as D,tr as O,wn as k,yn as A}from"./element-plus-Bx3j6vzG.js";import{p as j}from"./vendor-CyWsJd6S.js";import{t as M}from"./_plugin-vue_export-helper-C_bQVzeb.js";import{i as N}from"./monaco-DkJuNcCr.js";import{n as P,r as F}from"./flow-mindmap-OPE_D4da.js";var I=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`ico`,`svg`]);function L(e){if(!e)return!1;let t=e.split(`.`).pop()?.toLowerCase()??``;return I.has(t)}function ee(e){return`/api/editor/raw?path=${encodeURIComponent(e)}`}var R=new Set([`doc`,`docx`,`xls`,`xlsx`,`ppt`,`pptx`,`odt`,`ods`,`odp`]);function z(e){return e.split(/[\\/]/).pop()?.split(`.`).pop()?.toLowerCase()||``}function B(e){return R.has(z(e))}function V(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/preview?${n.toString()}`}function H(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/raw?${n.toString()}`}g(),p(),E();var te={class:`image-preview-toolbar`},U=[`title`],W={key:0,class:`image-preview-dim`},G={key:1,class:`image-preview-dim`},K={key:0,class:`image-preview-error`},q={class:`image-preview-error-path`},J=[`src`,`alt`],Y={class:`image-preview-footer`},X={class:`image-preview-zoom`},Z=M(f({__name:`ImagePreview`,props:{filePath:{},fileName:{}},setup(e){let i=e,d=S(null),f=S(1),p=S(0),m=S(0),g=S(!1),v=S({x:0,y:0}),E=S(null),O=S(!1),A=S(null),j=()=>{if(!d.value||!E.value)return;let e=d.value.clientWidth-32,t=d.value.clientHeight-32;if(e<=0||t<=0)return;let n=Math.min(e/E.value.w,t/E.value.h,1);f.value=n,p.value=0,m.value=0},M=()=>{f.value=Math.min(f.value*1.2,8)},N=()=>{f.value=Math.max(f.value/1.2,.1)},P=()=>{f.value=1,p.value=0,m.value=0,j()},F=e=>{e.preventDefault(),e.deltaY<0?M():N()},I=e=>{e.button===0&&(g.value=!0,v.value={x:e.clientX-p.value,y:e.clientY-m.value})},L=e=>{g.value&&(p.value=e.clientX-v.value.x,m.value=e.clientY-v.value.y)},R=()=>{g.value=!1},z=e=>{let t=e.target;E.value={w:t.naturalWidth,h:t.naturalHeight},O.value=!1,requestAnimationFrame(()=>j())},B=()=>{O.value=!0},V=n(()=>i.fileName||i.filePath?.split(`/`).pop()||``),H=n(()=>ee(i.filePath));return s(()=>{window.addEventListener(`mousemove`,L),window.addEventListener(`mouseup`,R),window.addEventListener(`resize`,j)}),l(()=>{window.removeEventListener(`mousemove`,L),window.removeEventListener(`mouseup`,R),window.removeEventListener(`resize`,j)}),T(()=>i.filePath,()=>{E.value=null,O.value=!1,f.value=1,p.value=0,m.value=0}),(e,n)=>{let s=o;return y(),r(`div`,{class:`image-preview`,ref_key:`containerRef`,ref:d,onWheel:F},[k(`div`,te,[k(`span`,{class:`image-preview-name`,title:i.filePath},h(V.value),9,U),E.value?(y(),r(`span`,W,h(E.value.w)+` × `+h(E.value.h)+` px`,1)):(y(),r(`span`,G,h(Math.round(f.value*100))+`%`,1)),n[0]||=k(`div`,{class:`image-preview-spacer`},null,-1),u(s,{content:`放大`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:M},[u(a(c),null,{default:C(()=>[u(a(x))]),_:1})])]),_:1}),u(s,{content:`缩小`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:N},[u(a(c),null,{default:C(()=>[u(a(w))]),_:1})])]),_:1}),u(s,{content:`适应窗口`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:P},[u(a(c),null,{default:C(()=>[u(a(t))]),_:1})])]),_:1})]),k(`div`,{class:D([`image-preview-stage`,{"is-dragging":g.value}]),onMousedown:I},[O.value?(y(),r(`div`,K,[u(a(c),{size:`32`,color:`var(--color-danger)`},{default:C(()=>[u(a(_))]),_:1}),n[1]||=k(`p`,null,`图片加载失败`,-1),k(`p`,q,h(i.filePath),1)])):(y(),r(`img`,{key:1,ref_key:`imgRef`,ref:A,src:H.value,alt:V.value,class:`image-preview-img`,style:b({transform:`translate(${p.value}px, ${m.value}px) scale(${f.value})`}),onLoad:z,onError:B,draggable:`false`},null,44,J))],34),k(`div`,Y,[n[2]||=k(`span`,null,`滚轮缩放 · 拖拽平移 · 双击重置`,-1),k(`span`,X,h(Math.round(f.value*100))+`%`,1)])],544)}}}),[[`__scopeId`,`data-v-28b3ff72`]]);g(),E();var ne={class:`md-preview`},re=[`innerHTML`],ie={key:1,class:`md-mindmap`},ae=M(f({__name:`MarkdownPreview`,props:{content:{},allowHtml:{type:Boolean,default:!0}},setup(e){let t=e;function i(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);return t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, form, input, button, textarea, select`).forEach(e=>e.remove()),t.content.querySelectorAll(`*`).forEach(e=>{for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase();if(n.startsWith(`on`)||n===`style`||n===`srcdoc`){e.removeAttribute(t.name);continue}if(n===`href`||n===`src`||n===`xlink:href`){let r=t.value.trim().replace(/[\u0000-\u0020]+/g,``);/^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i.test(r)||n===`src`&&/^data:image\/(?:png|gif|jpe?g|webp);base64,/i.test(r)||e.removeAttribute(t.name)}}e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`)}),t.innerHTML}let o=n(()=>{let e=t.content||``,n=[],r=`@@FLOWDASH_MINDMAP_BLOCK_`,a=e.replace(/```[ \t]*mindmap[ \t]*\n([\s\S]*?)```/gi,(e,t)=>{let i=n.length;return n.push({id:i,md:t}),`${r}${i}@@`}),o=j(a,{className:!1,html:t.allowHtml}),s=t.allowHtml?o:i(o),c=RegExp(`(?:<p>)?\\s*${r}(\\d+)@@\\s*(?:</p>)?`,`g`),l=[],u=0,d;for(;(d=c.exec(s))!==null;){d.index>u&&l.push({type:`html`,html:s.slice(u,d.index)});let e=Number(d[1]);l.push({type:`mindmap`,id:e,md:n[e].md,data:P(n[e].md)}),u=d.index+d[0].length}return u<s.length&&l.push({type:`html`,html:s.slice(u)}),l.length===0&&l.push({type:`html`,html:`<p class="md-empty">无内容</p>`}),l});return(e,t)=>(y(),r(`div`,ne,[(y(!0),r(A,null,m(o.value,(e,n)=>(y(),r(A,{key:n},[e.type===`html`?(y(),r(`div`,{key:0,class:`md-segment`,innerHTML:e.html},null,8,re)):(y(),r(`div`,ie,[t[0]||=k(`div`,{class:`md-mindmap-title`},`四维导图`,-1),u(a(F),{data:e.data,"preview-mode":``,class:`md-mindmap-canvas`},null,8,[`data`])]))],64))),128))]))}}),[[`__scopeId`,`data-v-9514c256`]]);g(),p(),E();var oe={class:`office-preview`},se={class:`office-preview-toolbar`},ce={class:`office-preview-badge`},le={key:0,class:`office-preview-side`},ue={key:0,class:`office-preview-state`},Q={key:1,class:`office-preview-state office-preview-error`},de=[`src`],fe=M(f({__name:`OfficePreview`,props:{filePath:{},source:{default:`worktree`},rev:{default:``},side:{default:`new`}},setup(t){let a=t,o=d(()=>N(()=>import(`./office-docx-B0sJQnr-.js`).then(t=>e(t.t(),1)),__vite__mapDeps([0,1,2,3]))),s=d(()=>N(()=>import(`./office-excel-DMRySOdK.js`).then(t=>e(t.t(),1)),__vite__mapDeps([4,1,0,2,3]))),c=d(()=>N(()=>import(`./office-pptx-Do2blxao.js`).then(t=>e(t.t(),1)),__vite__mapDeps([5,1,0,2,3]))),u=n(()=>z(a.filePath)),f=n(()=>u.value===`docx`?o:u.value===`xls`||u.value===`xlsx`?s:u.value===`pptx`?c:null),p=n(()=>H(a.filePath,{source:a.source,rev:a.rev,side:a.side})),m=n(()=>V(a.filePath,{source:a.source,rev:a.rev,side:a.side})),g=S(null),_=S(``),b=S(!1),x=S(!1),C=S(!1),w=0;function E(){_.value&&URL.revokeObjectURL(_.value),_.value=``}async function D(e){try{let t=await fetch(m.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;_.value=URL.createObjectURL(await t.blob()),C.value=!0,x.value=!1}catch{e===w&&(x.value=!0)}}async function A(){let e=++w;if(b.value=!0,x.value=!1,C.value=!1,g.value=null,E(),!f.value){await D(e),e===w&&(b.value=!1);return}try{let t=await fetch(p.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;g.value=await t.arrayBuffer()}catch{await D(e)}finally{e===w&&(b.value=!1)}}function j(){if(!C.value){let e=w;g.value=null,b.value=!0,D(e).finally(()=>{e===w&&(b.value=!1)})}}function M(){A()}return T(p,()=>{A()},{immediate:!0}),l(()=>{w++,E()}),(e,n)=>(y(),r(`div`,oe,[k(`div`,se,[n[0]||=k(`span`,{class:`office-preview-title`},`Office 预览`,-1),k(`span`,ce,h(u.value.toUpperCase()),1),t.source===`git`?(y(),r(`span`,le,h(t.side===`old`?`旧版本`:`新版本`),1)):i(``,!0),k(`button`,{class:`office-preview-reload`,type:`button`,onClick:M},`刷新`)]),b.value?(y(),r(`div`,ue,`正在生成预览…`)):x.value?(y(),r(`div`,Q,[...n[1]||=[k(`p`,null,`无法预览此 Office 文件。`,-1),k(`p`,{class:`office-preview-hint`},`支持 docx、xls/xlsx、pptx;老格式需要安装 LibreOffice 才能兜底预览。`,-1)]])):C.value?(y(),r(`iframe`,{key:2,class:`office-preview-frame`,src:_.value,title:`Office 文件 PDF 预览`},null,8,de)):f.value&&g.value?(y(),v(O(f.value),{key:3,class:`office-preview-document`,src:g.value,onError:j},null,40,[`src`])):i(``,!0)]))}}),[[`__scopeId`,`data-v-89cd4207`]]),pe=`allow-scripts allow-forms allow-popups allow-modals`,$=`<script>(function(){
3
3
  try{window.localStorage.getItem('__zk')}catch(e){
4
4
  var d={};var s={getItem:function(k){return Object.prototype.hasOwnProperty.call(d,k)?d[k]:null},setItem:function(k,v){d[k]=String(v)},removeItem:function(k){delete d[k]},clear:function(){d={}},key:function(i){var ks=Object.keys(d);return i<ks.length?ks[i]:null}};
5
5
  Object.defineProperty(window,'localStorage',{get:function(){return s},configurable:true});
@@ -10,25 +10,25 @@
10
10
  <meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
11
11
  <title>Zen GitSync</title>
12
12
  <!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
13
- <script type="module" crossorigin src="/assets/index-BHYAwndE.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-CFX8sK4w.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/element-plus-Bx3j6vzG.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/monaco-DkJuNcCr.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-CyWsJd6S.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-N8Vc5eEs.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-D0hK-p1U.js">
20
- <link rel="modulepreload" crossorigin href="/assets/IconButton-CFJkDLVG.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-BFb0I8eP.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C_bQVzeb.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-xtz4vvc9.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/IconButton-BShiC2AP.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-DJH7OlB_.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
23
- <link rel="modulepreload" crossorigin href="/assets/gitStore-grKwgyHH.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/gitStore-ByP9lpuT.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/fileIcon-B5p5pGN9.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-OPE_D4da.js">
26
- <link rel="modulepreload" crossorigin href="/assets/previewSandbox-D4455rKO.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/previewSandbox-DSzZ_Rky.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DYxFoHh1.js">
28
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-DMWtstZq.js">
29
- <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-CvN5b1ZC.js">
28
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-BEm2cK1X.js">
29
+ <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-CYxcLw10.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/toolsStore-B49mCaQ_.js">
31
- <link rel="modulepreload" crossorigin href="/assets/TemplateManager-XoPZv_i6.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/TemplateManager-DhKTZ4EM.js">
32
32
  <link rel="modulepreload" crossorigin href="/assets/vue-flow-fVVGZspa.js">
33
33
  <link rel="stylesheet" crossorigin href="/assets/element-plus-Qexo5hiM.css">
34
34
  <link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
@@ -0,0 +1,32 @@
1
+ 任务结束提示音 —— 音源与许可
2
+ ================================
3
+
4
+ 本目录下的两个 wav 取自 Kenney《Interface Sounds》音效包,许可为
5
+ Creative Commons Zero (CC0 1.0) —— 公共领域,可自由用于个人 / 教育 / 商业项目,
6
+ **无需署名**(这里保留出处仅作为溯源记录)。
7
+
8
+ 原包页面:https://kenney.nl/assets/interface-sounds
9
+ 许可全文:https://creativecommons.org/publicdomain/zero/1.0/
10
+ 本目录文件的来源镜像:https://github.com/Calinou/kenney-interface-sounds
11
+ (Kenney 原包的 ogg → wav 无损转封装,Godot 用)
12
+
13
+ 文件对应关系
14
+ ------------
15
+
16
+ task-done.wav ← confirmation_001.wav 0.29s 44.1kHz 单声道 16bit 任务正常跑完
17
+ task-error.wav ← error_005.wav 0.52s 44.1kHz 单声道 16bit 任务执行出错
18
+
19
+ 为什么选 CC0
20
+ ------------
21
+
22
+ 本仓库以 Apache-2.0 发布到 npm,构建产物里会带上 public/ 下的全部资源。
23
+ 带署名条款(CC BY)、相同方式共享(CC BY-SA)或"禁止再分发原始文件"条款
24
+ (Mixkit / Pixabay / Envato 系)的音源混进发行包都会造成许可证污染 ——
25
+ CC0 是唯一零负担的选择。
26
+
27
+ 换一个音色
28
+ ----------
29
+
30
+ 上面那个 GitHub 镜像里有 100 个同许可的音效(click / confirmation / glass /
31
+ error / toggle / maximize 等),直接下载覆盖同名文件即可,代码侧不需要改动。
32
+ 想换路径或改成别的音源,改 src/ui/client/src/utils/taskSound.ts 里的 SOUND_SRC。
@@ -1047,10 +1047,13 @@ export function registerConfigRoutes({
1047
1047
  if (!rawConfig.ui.aiDiffSummaryByProject || typeof rawConfig.ui.aiDiffSummaryByProject !== 'object' || Array.isArray(rawConfig.ui.aiDiffSummaryByProject)) {
1048
1048
  rawConfig.ui.aiDiffSummaryByProject = {}
1049
1049
  }
1050
+ if (!rawConfig.ui.editorWorkspaceByProject || typeof rawConfig.ui.editorWorkspaceByProject !== 'object' || Array.isArray(rawConfig.ui.editorWorkspaceByProject)) {
1051
+ rawConfig.ui.editorWorkspaceByProject = {}
1052
+ }
1050
1053
 
1051
1054
  // 浅合并顶层 ui 字段(支持嵌套对象整体替换,如 commandConsole)
1052
1055
  for (const key of Object.keys(partial)) {
1053
- if (key === 'layoutsByProject' || key === 'aiDiffSummaryByProject') {
1056
+ if (key === 'layoutsByProject' || key === 'aiDiffSummaryByProject' || key === 'editorWorkspaceByProject') {
1054
1057
  // 深合并 map:保留其它项目条目
1055
1058
  const incoming = partial[key]
1056
1059
  if (incoming && typeof incoming === 'object' && !Array.isArray(incoming)) {
@@ -1274,10 +1274,15 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
1274
1274
  async function runProgressReport(trigger, { locale = '', intervalMs = 0 } = {}) {
1275
1275
  const facts = await collectRunningFacts();
1276
1276
 
1277
- // 没任务在跑时自动报告**静默跳过**:一条"什么都没发生"的报告每 10 分钟占一格,
1278
- // 只会把真正有用的那几条挤出历史(上限 20 条)。
1279
- // 手动「立即报告」不受此限 —— 那时用户要的就是"现在确实没有东西在跑"这句实话。
1280
- if (!facts.length && trigger === 'auto') return null;
1277
+ // 没有任务在跑就**不生成报告**,自动与手动一视同仁。
1278
+ // · 自动:一条"什么都没发生"每 10 分钟占一格,只会把真正有用的挤出历史(上限 20 条);
1279
+ // · 手动:用户点这一下是想知道"现在跑到哪一步了",而此刻确实没有东西在跑 ——
1280
+ // 前端弹一句"当前没有正在执行的任务"(useOrchestrator.generateReport)比往历史里
1281
+ // 塞一条空记录有用:空记录回头翻的时候零信息量,还会把有用的挤出去。
1282
+ // (用户 2026-09-29 报的问题:历史里 4 条"当时没有任务在执行"两两重复,
1283
+ // 点一下「立即报告」就多两条 —— 空报告本机几十毫秒返回,前端的防抖拦不住第二次点击。)
1284
+ // 返回 null 是"这次不该报"的正常结果,不是错误。
1285
+ if (!facts.length) return null;
1281
1286
  if (trigger === 'auto' && !(await claimReportSlot(intervalMs))) return null;
1282
1287
 
1283
1288
  const ctx = await readReportContext();
@@ -1312,8 +1317,12 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
1312
1317
  /**
1313
1318
  * 手动生成一份报告。body: { locale? }
1314
1319
  *
1315
- * 与自动报告共用同一条链路,只在两处不同:跳过"必须有任务在跑"的静默跳过、
1316
- * 不占用自动报告的间隔名额(否则用户点一下「立即报告」,下一次自动报告被推迟一整个间隔)。
1320
+ * 与自动报告共用同一条链路,只在两处不同:
1321
+ * · 不占用自动报告的间隔名额(否则用户点一下「立即报告」,下一次自动报告被推迟一整个间隔);
1322
+ * · 间隔窗口内**内容一样**的第二份不重复落盘(连点两下只留一份,见 appendReport)。
1323
+ *
1324
+ * 没有任务在跑时同样返回 `report: null` —— 这时前端弹一句"当前没有正在执行的任务",
1325
+ * 而不是往历史里记一条空报告(理由见 runProgressReport)。
1317
1326
  */
1318
1327
  app.post('/api/workbench/orchestrator/report', asyncRoute(async (req, res) => {
1319
1328
  const locale = typeof req.body?.locale === 'string' ? req.body.locale : '';
@@ -0,0 +1,274 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // 「这一轮执行现在跑到哪了」—— 从执行记录里抽出可读事实的**唯一实现**。
16
+ //
17
+ // 2026-09-29 从 progressReport.js 拆出来。同一批事实现在有两个消费者:
18
+ // 1. 进度报告(progressReport.buildRunningFacts → 喂给主 Agent 写汇报);
19
+ // 2. 看板「进行中」卡片(projectRegistry.decorateTaskForBoard → 直接给人看)。
20
+ // 留在 progressReport.js 里会让 projectRegistry 反向依赖报告模块,而报告模块本来就依赖
21
+ // projectRegistry(用它的 projectName)—— 拆成独立模块比引一条循环依赖省事得多:
22
+ // 这几段文本本来就只跟 job 有关,跟"报告"无关。
23
+ //
24
+ // 两处口径必须逐字一致:同一件事在报告里写「最近思考:…」、在卡片上换了另一句话,
25
+ // 用户看到的是**同一条任务的两个说法**,只会以为自己看错了。
26
+
27
+ /** 一句话的最大长度(最近思考 / 最近输出都走它) */
28
+ export const MAX_LINE_CHARS = 160;
29
+ /** 最近一次工具调用的参数摘要长度 */
30
+ export const MAX_TOOL_CHARS = 100;
31
+ export const MAX_TOOL_NAME_CHARS = 60;
32
+ /** 工具名分布串的长度上限(`Bash×14 · Read×5 · Edit`) */
33
+ export const MAX_MIX_CHARS = 160;
34
+
35
+ /** 工具分布只看最近这么多次调用:整轮上千次的话,早先的模式早就被淹没了 */
36
+ export const TOOL_MIX_WINDOW = 20;
37
+ /**
38
+ * 静默多久才算"值得写进事实"(毫秒)。
39
+ * 低于这个数的"刚刚还在产出"不是事实、只是噪声 —— 写进 prompt 会挤掉有用的话,
40
+ * 显示在卡片上会让每一条都挂个"静默 2 秒"。
41
+ */
42
+ export const SILENT_NOTABLE_MS = 60 * 1000;
43
+
44
+ /** 输出里的最后一行有效文本 —— 模型最近说的一句话,比第一行更能说明"现在在干嘛" */
45
+ export function tailLine(output) {
46
+ const text = typeof output === 'string' ? output : '';
47
+ if (!text) return '';
48
+ const lines = text.split('\n');
49
+ for (let i = lines.length - 1; i >= 0; i--) {
50
+ const line = lines[i].replace(/\s+/g, ' ').trim();
51
+ if (line) return line.slice(0, MAX_LINE_CHARS);
52
+ }
53
+ return '';
54
+ }
55
+
56
+ /**
57
+ * 「最后回复」摘录最多多少字。
58
+ *
59
+ * 100 是按"卡片上放得下多少"倒推的:卡片宽 200–500px、字号 var(--font-size-xs),
60
+ * 一行约 25–35 个汉字、卡片最多显示 3 行(.kb-card__reply 的 line-clamp),
61
+ * 于是 2000px 宽的窗口(本机实测)刚好整段显示得完;窗口更窄时尾部会被 CSS 截掉
62
+ * 几个字(`…` 是诚实的标记,完整摘录在悬停提示里)。
63
+ *
64
+ * 这个数**不能再小**:截断的是头部,而模型收尾那句反问(「要 push 吗?」)常常落在
65
+ * 倒数第 80–100 字之间 —— 给 80 字就会把它整句丢掉,反而是这个功能最不该丢的东西。
66
+ * 也不能再大:多出来的部分只会被 CSS 从尾巴切掉,白白把有用的开头挤出去。
67
+ */
68
+ export const MAX_REPLY_CHARS = 100;
69
+
70
+ /**
71
+ * 摘录前先切的尾部窗口。
72
+ *
73
+ * 为什么要先切窗口再分行:单条 job 的 output 上限是 100MB(taskRunner 的 MAX_OUTPUT),
74
+ * 而看板是 5s 轮询一次、每次都要给几十张卡片算这个字段 —— 对着整份输出 split 一遍是
75
+ * 白白烧 CPU(切片是 O(1) 的 SlicedString,split 不是)。4000 字符足够覆盖一整段收尾。
76
+ */
77
+ export const REPLY_WINDOW_CHARS = 4000;
78
+
79
+ /** 纯装饰行(表格分隔线 / 水平线),留着只是噪声 */
80
+ const DECOR_LINE = /^\s*[|:\-\s=]+\s*$/;
81
+ /** Markdown 表格行(`| 项 | 结果 |`):压成一行文字后只剩一堆竖线,整行丢掉 */
82
+ const TABLE_LINE = /^\s*\|.*\|\s*$/;
83
+ /** 行首的标题 / 列表 / 引用标记 —— 压平成一句话之后它们不再是结构,只是噪声 */
84
+ const LINE_MARKER = /^\s*(?:#{1,6}\s+|[-*+]\s+|>\s*)/;
85
+ /** 行内强调与行内代码的标记。**不**去掉 `_`:它会命中 snake_case 的标识符 */
86
+ const INLINE_MARKER = /\*\*|`/g;
87
+
88
+ /**
89
+ * 从执行输出里摘出「最后说了什么」—— 看板「已完成」卡片上那一行。
90
+ *
91
+ * 与 tailLine 的分工:tailLine 是**正在跑**时用的,只要最新那一句("它现在在干嘛");
92
+ * 这里是**跑完之后**用的,要一小段("它最后交代了什么")。后者才是"这任务是不是真完了"
93
+ * 的证据 —— 模型经常在收尾时反问一句「要 push 吗?」,只看标题的话,用户根本不知道
94
+ * 这条已经完成的任务其实在等他一句话。
95
+ *
96
+ * 三条规矩:
97
+ * 1. 只取**尾部**:一段回复的重点在收尾,不在开头;
98
+ * 2. 丢掉表格行、行首标记、`**`/反引号,压平成一句话(卡片上没有富文本,留着只是噪声);
99
+ * 3. 真截断过才在前面加 `…`(没截就不加,`…` 是"这句话从中间开始"的诚实标记)。
100
+ */
101
+ export function tailExcerpt(output, { maxChars = MAX_REPLY_CHARS, windowChars = REPLY_WINDOW_CHARS } = {}) {
102
+ const raw = typeof output === 'string' ? output : '';
103
+ if (!raw) return '';
104
+
105
+ // 窗口起点多半落在半句话中间:从窗口内的第一个换行之后开始取,摘录才不会以半句开头。
106
+ // 窗口内一个换行都没有(单行超长输出)时只能整段用,此时下面的截断会补上 `…`。
107
+ // 没切窗口(输出本来就短)时不动第一行 —— 那是完整的一句,不是半截。
108
+ const cutWindow = raw.length > windowChars;
109
+ const windowed = cutWindow ? raw.slice(-windowChars) : raw;
110
+ const body = cutWindow ? windowed.slice(windowed.indexOf('\n') + 1) : windowed;
111
+
112
+ const flat = body
113
+ .split('\n')
114
+ .filter(line => !DECOR_LINE.test(line) && !TABLE_LINE.test(line))
115
+ .map(line => line.replace(LINE_MARKER, '').replace(INLINE_MARKER, '').replace(/\s+/g, ' ').trim())
116
+ .filter(Boolean)
117
+ .join(' ');
118
+
119
+ if (!flat) return '';
120
+ // 从尾部取:上面已经说明过为什么宁丢开头不丢结尾
121
+ return flat.length > maxChars ? `…${flat.slice(-maxChars).trim()}` : flat;
122
+ }
123
+
124
+ /**
125
+ * 最近一次工具调用的一句话描述(如 `Edit src/ui/client/src/App.vue`)。
126
+ * 从后往前找:数组里最后一条未必带 name(老数据 / 半截记录),跳过它们而不是返回空。
127
+ */
128
+ export function describeLastTool(toolCalls) {
129
+ const list = Array.isArray(toolCalls) ? toolCalls : [];
130
+ for (let i = list.length - 1; i >= 0; i--) {
131
+ const call = list[i];
132
+ if (!call || !call.name) continue;
133
+ const name = String(call.name).slice(0, MAX_TOOL_NAME_CHARS);
134
+ const raw = typeof call.argsPreview === 'string' && call.argsPreview
135
+ ? call.argsPreview
136
+ : (typeof call.arguments === 'string' ? call.arguments : '');
137
+ const args = String(raw || '').replace(/\s+/g, ' ').trim().slice(0, MAX_TOOL_CHARS);
138
+ return args ? `${name} ${args}` : name;
139
+ }
140
+ return '';
141
+ }
142
+
143
+ /**
144
+ * 最近 N 次工具调用的名字分布(`Bash×14 · Read×5`,从多到少)。
145
+ *
146
+ * 为什么需要它:只给"最近一次调用"时,看不出 119 次里有 118 次在干同一件事 ——
147
+ * 于是只能写"无法判断是在改代码还是反复读文件"。名字为空的老记录 / 半截记录跳过,
148
+ * 但不占窗口名额(否则一串空名字会把整段分布挤没)。
149
+ */
150
+ export function describeToolMix(toolCalls) {
151
+ const list = Array.isArray(toolCalls) ? toolCalls : [];
152
+ const counts = new Map();
153
+ let seen = 0;
154
+ for (let i = list.length - 1; i >= 0 && seen < TOOL_MIX_WINDOW; i--) {
155
+ const call = list[i];
156
+ if (!call || !call.name) continue;
157
+ seen++;
158
+ const name = String(call.name).slice(0, MAX_TOOL_NAME_CHARS);
159
+ counts.set(name, (counts.get(name) || 0) + 1);
160
+ }
161
+ if (!counts.size) return '';
162
+ return [...counts.entries()]
163
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
164
+ .map(([name, n]) => (n > 1 ? `${name}×${n}` : name))
165
+ .join(' · ')
166
+ .slice(0, MAX_MIX_CHARS);
167
+ }
168
+
169
+ /**
170
+ * 距最后一次产出(正文 / 思考 / 工具调用)多久。
171
+ * 没有 `lastActivityAt`(老记录、别实例上的旧版进程)或还没到阈值 → null,
172
+ * 调用方据此决定"这条不显示",而不是显示一个假的 0。
173
+ */
174
+ export function silentMsOf(job, now) {
175
+ const last = Date.parse((job && job.lastActivityAt) || '');
176
+ if (!Number.isFinite(last)) return null;
177
+ const ms = Math.max(0, now - last);
178
+ return ms >= SILENT_NOTABLE_MS ? ms : null;
179
+ }
180
+
181
+ /** job 是否算"正在跑"(看板卡片只在此时显示活动摘要) */
182
+ export function isLiveJob(job) {
183
+ return !!job && (job.status === 'running' || job.status === 'pending');
184
+ }
185
+
186
+ /**
187
+ * 本轮跑的是哪个执行器(`claude` | `opencode` | `codex`)。
188
+ *
189
+ * 只在**认出是已知执行器**时才回值:job.agent 来自 jobs.json(会被手工编辑、也读过
190
+ * 别实例写的旧记录),回一个前端不认识的字符串只会让卡片上的图标位空着或显示错品牌。
191
+ * 认不出 → 空串,调用方据此不渲染图标,而不是回落成"猜一个 claude"——
192
+ * 猜错的执行器比不显示更糟。
193
+ *
194
+ * 这里**刻意不 import taskRunner 的 TASK_EXECUTORS**:taskRunner 拉起进程、依赖一堆
195
+ * 运行时模块,而本模块被 projectRegistry(5s 轮询的看板)直接引用,没必要为了三个
196
+ * 字符串把整条执行链拖进来。新增执行器时这里和 taskRunner 一起改。
197
+ */
198
+ const KNOWN_EXECUTORS = ['claude', 'opencode', 'codex'];
199
+
200
+ export function jobAgent(job) {
201
+ const v = job && typeof job.agent === 'string' ? job.agent.trim().toLowerCase() : '';
202
+ return KNOWN_EXECUTORS.includes(v) ? v : '';
203
+ }
204
+
205
+ /**
206
+ * 单条 job → 卡片 / 报告可用的活动摘要。非 running/pending 返回 null(调用方据此不显示)。
207
+ *
208
+ * 时间戳用**毫秒时长**(elapsedMs / silentMs)而不是 ISO:
209
+ * 前端两个视图都拿 formatDurationMs 格式化,跨实例读来的记录也一样能算出时长。
210
+ *
211
+ * @param {object} job 执行记录(内存里的原始对象,或 projectJob 投影)
212
+ * @param {number} now 参照时刻,便于单测
213
+ */
214
+ export function buildLiveActivity(job, now = Date.now()) {
215
+ if (!isLiveJob(job)) return null;
216
+ const started = Date.parse(job.startedAt || '');
217
+ return {
218
+ jobId: job.id || '',
219
+ status: job.status,
220
+ /** 本轮执行器(claude | opencode | codex)。老记录没有这个字段 → 空串 */
221
+ agent: jobAgent(job),
222
+ startedAt: job.startedAt || null,
223
+ elapsedMs: Number.isFinite(started) ? Math.max(0, now - started) : 0,
224
+ /** 见 taskRunner 的 MAX_TOOL_CALLS:超上限的调用不再入数组,所以这是"至少这么多次" */
225
+ toolCallCount: Array.isArray(job.toolCalls) ? job.toolCalls.length : 0,
226
+ lastTool: describeLastTool(job.toolCalls),
227
+ toolMix: describeToolMix(job.toolCalls),
228
+ /** 最近一段思考。多数任务一句正文都不写,它是"它在干嘛"最直接的证据 */
229
+ lastThought: tailLine(job.thinking),
230
+ /** 最新的回复(正文最后一行),空串 = 这轮还没写过正文 */
231
+ lastLine: tailLine(job.output),
232
+ silentMs: silentMsOf(job, now),
233
+ };
234
+ }
235
+
236
+ /** 最近一次产出的时刻,取不到时回落到启动时刻 */
237
+ function activityTimestamp(job) {
238
+ return Date.parse(job.lastActivityAt || '') || Date.parse(job.startedAt || '') || 0;
239
+ }
240
+
241
+ /**
242
+ * 一个任务可能同时有好几条 job 在跑(并行的子任务 / 连点两次执行),
243
+ * 卡片只放得下一条 —— 挑**最近有动静的**那条:用户问"现在在干嘛"时,
244
+ * 刚吐过字的那个才是答案,跑得最久的那个未必。
245
+ *
246
+ * 时间戳打平时按原顺序取末条(与 latestJob 同口径)。
247
+ *
248
+ * @returns {object|null} buildLiveActivity 的结果;没有在跑的 job 时 null
249
+ */
250
+ export function pickLiveActivity(jobsForTask, now = Date.now()) {
251
+ const list = Array.isArray(jobsForTask) ? jobsForTask : [];
252
+ let best = null;
253
+ let bestAt = -Infinity;
254
+ for (const j of list) {
255
+ if (!isLiveJob(j)) continue;
256
+ const at = activityTimestamp(j);
257
+ if (at >= bestAt) {
258
+ best = j;
259
+ bestAt = at;
260
+ }
261
+ }
262
+ return best ? buildLiveActivity(best, now) : null;
263
+ }
264
+
265
+ export const __testables = {
266
+ tailLine,
267
+ tailExcerpt,
268
+ describeLastTool,
269
+ describeToolMix,
270
+ silentMsOf,
271
+ jobAgent,
272
+ buildLiveActivity,
273
+ pickLiveActivity,
274
+ };