specrails-desktop 2.29.3 → 2.29.4
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/client/dist/assets/{ActivityFeedPage-CFB4UBhT.js → ActivityFeedPage-CDMzpGXd.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-C-T0r16B.js → AgentBrowserCapture-BVTacapZ.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-IljV5FOQ.js → AgentModeCodePane-D9bhFyuy.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-B5miH10_.js → AgentModeJobsPane-DAjN8Au5.js} +1 -1
- package/client/dist/assets/{AgentsPage-BgYQrAcH.js → AgentsPage-CebykajE.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-BtVCuNjL.js → AnalyticsPage-BiPrVvgv.js} +1 -1
- package/client/dist/assets/{CodePage-Cb_jAjSV.js → CodePage-Dw9eQFRS.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-s_Cc_JL1.js → DesktopAnalyticsPage-cCG_JAEi.js} +1 -1
- package/client/dist/assets/{DocsDialog-Cr47QI9L.js → DocsDialog-C1kZ_mRP.js} +1 -1
- package/client/dist/assets/{DocsPage-C_WPRuG_.js → DocsPage-3GivxWJm.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-NfFOciRB.js → InteractiveJobComposer-KuZXGdr8.js} +1 -1
- package/client/dist/assets/{JobDetailModal-DqEW7Z7K.js → JobDetailModal-DWpRALkB.js} +1 -1
- package/client/dist/assets/{JobDetailPage-D4ppEpDE.js → JobDetailPage-mrXDKafj.js} +1 -1
- package/client/dist/assets/{JobsPage-DStSoMyq.js → JobsPage-CUYXjcoc.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-DltCwS4P.js → LoopBuilderPage-CwjBD5Gm.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-BWaiAbUq.js → LoopPreviewModal-CO9ton0-.js} +1 -1
- package/client/dist/assets/{LoopsPage-BqjbXuBZ.js → LoopsPage-CLL5UuvN.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext--o3JAM9F.js → MinimizedChatsContext-DJAkRYps.js} +1 -1
- package/client/dist/assets/{PluginsPage-B-UOmzXw.js → PluginsPage-DZvCC5Ri.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-DkGajIxE.js → ProjectSettingsDialog-C7Hesadg.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-5CUXlkw7.js → TemplatePreviewModal-DczCzUzP.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-B_igqmti.js → TicketDetailModal-CFh7GOv5.js} +3 -3
- package/client/dist/assets/{Trans-DDMIBBUd.js → Trans-Cm9rhdnG.js} +1 -1
- package/client/dist/assets/{dashboard-DAr6F9et.js → dashboard-CHCEkds4.js} +1 -1
- package/client/dist/assets/{dashboard-D4Uj-XWc2.js → dashboard-Cqmmbh3l2.js} +1 -1
- package/client/dist/assets/{dashboard-BYzxom0L.js → dashboard-CrlTV1ms.js} +1 -1
- package/client/dist/assets/dashboard-Cslu_GCH.js +1 -0
- package/client/dist/assets/{dashboard-CnNtJBQ5.js → dashboard-DUU8-_Rg.js} +1 -1
- package/client/dist/assets/{dashboard-vrjZvxWy.js → dashboard-DweT3Y4R.js} +1 -1
- package/client/dist/assets/{dashboard-D57uRaGw2.js → dashboard-Dy2Um-US2.js} +1 -1
- package/client/dist/assets/{dashboard-vAcsn0L3.js → dashboard-WhzOQw1S.js} +1 -1
- package/client/dist/assets/{dist-js-DcoXTnEI.js → dist-js-BERxOT0X.js} +1 -1
- package/client/dist/assets/{dist-js-BCUhGUXS.js → dist-js-Di8lflPo.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-DP3Ab6fX.js → formatDistanceToNow-BAXZRbEO.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DdKKL8Fx.js → getTimezoneOffsetInMilliseconds-DwzZXHtg.js} +1 -1
- package/client/dist/assets/{i18n-DREQCE2F.js → i18n-cK5pPbj8.js} +2 -2
- package/client/dist/assets/{index-CQBIemiz.css → index-BT6xDQjB.css} +1 -1
- package/client/dist/assets/index-CCq_kIrF.js +76 -0
- package/client/dist/assets/{spending-CtY7yYes.js → spending-BLb5pj6G.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-BFBdW59q.js → useBrowserCaptureSession-DR1_jL56.js} +1 -1
- package/client/dist/assets/{useDesktop-uWVZ8eia.js → useDesktop-D4DxBGPU.js} +1 -1
- package/client/dist/index.html +12 -12
- package/docs/internals/safe-pr-review-flow.md +32 -3
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +115 -1
- package/server/dist/agent-operator-prompt.js +9 -0
- package/server/dist/mcp/tools/rails.js +8 -0
- package/server/dist/project-git.js +7 -2
- package/server/dist/rail-isolated-launch.js +82 -23
- package/server/dist/rail-pr-decision.js +8 -0
- package/server/dist/rail-worktree-release.js +88 -2
- package/server/dist/rails-router.js +99 -1
- package/server/dist/worktree-manager.js +32 -5
- package/client/dist/assets/dashboard-BmDZmHyY.js +0 -1
- package/client/dist/assets/index-B784m8RT.js +0 -76
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e}from"./i18n-
|
|
1
|
+
import{o as e}from"./i18n-cK5pPbj8.js";var t={job:`analytics:surfaces.job`,"quick-spec":`analytics:surfaces.quickSpec`,"explore-spec":`analytics:surfaces.exploreSpec`,"ai-edit":`analytics:surfaces.aiEdit`,smash:`analytics:surfaces.smash`,"file-summary":`analytics:surfaces.fileSummary`,loop:`analytics:surfaces.loop`,"chat-sidebar":`analytics:surfaces.chatSidebar`,"spec-launcher":`analytics:surfaces.specLauncher`,proposal:`analytics:surfaces.proposal`,"agent-studio":`analytics:surfaces.agentStudio`,setup:`analytics:surfaces.setup`},n={bg:`bg-muted/40`,text:`text-muted-foreground`,ring:`ring-border/40`,dot:`bg-muted-foreground`},r={job:{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"quick-spec":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},"explore-spec":{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"ai-edit":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},smash:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"file-summary":{bg:`bg-accent-warning/15`,text:`text-accent-warning`,ring:`ring-accent-warning/40`,dot:`bg-accent-warning`},loop:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`},"chat-sidebar":{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"spec-launcher":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},proposal:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"agent-studio":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},setup:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`}};function i(e,t){return new Proxy(e,{get(e,n,r){return typeof n==`string`&&!(n in e)?t(n):Reflect.get(e,n,r)}})}var a=i(Object.defineProperties({},Object.fromEntries(Object.keys(t).map(n=>[n,{get:()=>e.t(t[n]),enumerable:!0}]))),e=>e),o=i(r,()=>n);export{a as n,o as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t}from"./jsx-runtime-zu2_FqZY.js";import{o as n}from"./i18n-DREQCE2F.js";import{t as r}from"./createLucideIcon-Bjbbp_KS.js";import{a as i,n as a}from"./auth-D_5bQv5W.js";import{t as o}from"./api-B2M7CtDw.js";var s=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),c=r(`globe`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]]),l=r(`rotate-cw`,[[`path`,{d:`M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8`,key:`1p45f6`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}]]),u=e(t(),1);function d(){return typeof window<`u`&&`__TAURI_INTERNALS__`in window||typeof window<`u`&&window.location.protocol!==`http:`&&window.location.protocol!==`https:`?`ws://localhost:4200`:`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`}var f=d();function p(){let e={BASE_URL:`/`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1}.VITE_FEATURE_BROWSER_CAPTURE;return typeof e!=`string`||e!==`false`}var m={desktop:{width:1280,height:800},tablet:{width:768,height:1024},mobile:{width:375,height:667}},h=class extends Error{},g=class extends Error{};function _(e){if(!e)throw Error(`A project owner is required for browser sessions`);return`${i}/api/projects/${encodeURIComponent(e)}`}async function v(e,t){let n=await fetch(`${_(e)}/browser/sessions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({initialUrl:t})});if(n.status===409)throw new h(`Too many open browser sessions`);if(n.status===502)throw new g(`The browser failed to launch`);if(!n.ok)throw Error(`Failed to open browser (${n.status})`);return(await n.json()).session}async function y(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/navigate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,url:r})});if(!i.ok)throw Error(`Navigation failed (${i.status})`);return await i.json()}async function b(e,t,n,r,i){let a=await fetch(`${_(e)}/browser/sessions/${t}/capture`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,pendingSpecId:r,captureNetwork:i?.captureNetwork??!0})});if(!a.ok)throw Error(await x(a));return await a.json()}async function x(e){let t=await e.json().then(e=>e.error).catch(()=>void 0);return t?`Capture failed (${e.status}): ${t}`:`Capture failed (${e.status})`}async function S(e,t,n,r,i,a=m){let o=await fetch(`${_(e)}/browser/sessions/${t}/capture-breakpoints`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,anchorPoint:r,pendingSpecId:i,breakpoints:a})});if(!o.ok)throw Error(await x(o));return await o.json()}async function C(e,t,n){let r=new FormData;r.append(`file`,t,n);let i=await fetch(`${o()}/tickets/${e}/attachments`,{method:`POST`,body:r});if(!i.ok)throw Error(`Upload failed (${i.status})`);return(await i.json()).attachment}async function w(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/clipboard`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,text:r})});if(!i.ok)throw Error(`Clipboard failed (${i.status})`);return await i.json()}async function T(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/element`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({selector:n,direction:r})});if(!i.ok)throw Error(`Element navigate failed (${i.status})`);return(await i.json()).probe}async function E(e,t,n){try{await fetch(`${_(e)}/browser/sessions/${t}/popup-view`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({target:n})})}catch{}}function D(e){if(!e||e===`about:blank`)return`…`;try{return new URL(e).hostname||`…`}catch{return`…`}}async function O(e,t){try{await fetch(`${_(e)}/browser/sessions/${t}`,{method:`DELETE`})}catch{}}function k(e,t){return`${f||(typeof window<`u`?`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`:`ws://localhost:4200`)}/ws/browser/${e}?projectId=${encodeURIComponent(t)}`}function A(e,t){let n=k(e,t),r=a(),i=r?new WebSocket(n,[`specrails-desktop`,r]):new WebSocket(n);return i.binaryType=`arraybuffer`,i}function j(e,t=e=>requestAnimationFrame(e),n=e=>cancelAnimationFrame(e)){let r=null,i=null,a=null,o=!1,s=()=>{a!=null&&(n(a),a=null);let t=r,o=i;r=null,i=null,t&&(!o||t.x!==o.x||t.y!==o.y)&&e({type:`mouse`,action:`move`,x:t.x,y:t.y}),o&&e({type:`wheel`,x:o.x,y:o.y,deltaX:o.deltaX,deltaY:o.deltaY})},c=()=>{a==null&&!o&&(a=t(()=>{a=null,s()}))};return{move(e,t){o||(r={x:e,y:t},c())},wheel(e,t,n,r){o||(i?(i.x=e,i.y=t,i.deltaX+=n,i.deltaY+=r):i={x:e,y:t,deltaX:n,deltaY:r},c())},flush(){o||s()},dispose(){o||(o=!0,a!=null&&(n(a),a=null),r=null,i=null)}}}function M(e,t,n){let r=t.width>0?(e.x-t.left)/t.width:0,i=t.height>0?(e.y-t.top)/t.height:0,a=Math.min(Math.max(r,0),1)*n.width,o=Math.min(Math.max(i,0),1)*n.height;return{x:Math.round(a),y:Math.round(o)}}function N(e,t,n){let r=n.width>0?t.width/n.width:0,i=n.height>0?t.height/n.height:0;return{left:t.left+e.x*r,top:t.top+e.y*i,width:e.width*r,height:e.height*i}}function P(e,t){return{x:Math.min(e.x,t.x),y:Math.min(e.y,t.y),width:Math.abs(e.x-t.x),height:Math.abs(e.y-t.y)}}function F(e,t=8){return e.width>=t&&e.height>=t}function I(e,t){let n=Math.max(1,t.width),r=Math.max(1,t.height),i=Math.max(0,Math.min(e.x,n-1)),a=Math.max(0,Math.min(e.y,r-1)),o=Math.min(n,Math.max(i+1,e.x+e.width)),s=Math.min(r,Math.max(a+1,e.y+e.height));return{x:Math.round(i),y:Math.round(a),width:Math.round(o-i),height:Math.round(s-a)}}function L(e){return{nodeCount:e.nodes.length,htmlBytes:e.html.length,truncated:e.htmlTruncated,networkCount:e.networkRequests?.length??0}}function R(e){let t=e.schedule??(e=>requestAnimationFrame(e)),n=e.cancel??(e=>cancelAnimationFrame(e)),r=e.now??(()=>typeof performance<`u`?performance.now():Date.now()),i=e.release??(()=>{}),a={received:0,decoded:0,drawn:0,droppedStale:0,droppedUndrawn:0,decodeMsTotal:0,drawMsTotal:0},o=!1,s=!1,c=null,l=null,u=null,d=()=>{u=null;let t=l;if(l=null,t==null||o){t!=null&&f(t);return}let n=r();try{e.draw(t),a.drawn++,a.drawMsTotal+=r()-n}catch{}finally{f(t)}},f=e=>{try{i(e)}catch{}},p=()=>{u==null&&!o&&(u=t(d))},m=t=>{s=!0;let n=r();e.decode(t).then(e=>{if(o){f(e);return}a.decoded++,a.decodeMsTotal+=r()-n,l!=null&&(a.droppedUndrawn++,f(l)),l=e,p(),h()},()=>{h()})},h=()=>{if(o){s=!1;return}let e=c;c=null,e==null?s=!1:m(e)};return{push(e){if(!o){if(a.received++,s){c!=null&&a.droppedStale++,c=e;return}m(e)}},stats(){return{...a}},dispose(){if(!o){if(o=!0,c=null,u!=null){try{n(u)}catch{}u=null}l!=null&&(f(l),l=null)}}}}var z=`specrails-desktop:browser-capture-debug`;function B(){try{return typeof localStorage<`u`&&localStorage.getItem(z)===`1`}catch{return!1}}function V(e,t,n){let r=Math.max(1,n),i=t.decoded-e.decoded,a=t.drawn-e.drawn,o=e=>Math.round(e*10)/10;return{fpsReceived:o((t.received-e.received)*1e3/r),fpsDrawn:o(a*1e3/r),droppedStale:t.droppedStale-e.droppedStale,droppedUndrawn:t.droppedUndrawn-e.droppedUndrawn,avgDecodeMs:i>0?o((t.decodeMsTotal-e.decodeMsTotal)/i):0,avgDrawMs:a>0?o((t.drawMsTotal-e.drawMsTotal)/a):0}}function H(e,t=e=>console.table([e]),n=2e3,r=()=>Date.now()){let i=e(),a=r(),o=setInterval(()=>{let n=e(),o=r();t(V(i,n,o-a)),i=n,a=o},n);return()=>clearInterval(o)}function U(e){let{projectId:t,open:r,initialUrl:i}=e,a=(0,u.useRef)(null),o=(0,u.useRef)(null),s=(0,u.useRef)(null),c=(0,u.useRef)(0),l=(0,u.useRef)(new Map),d=(0,u.useCallback)(()=>{if(c.current!==0||l.current.size===0)return;let e=[...l.current.values()];l.current.clear();for(let t of e)O(t.projectId,t.sessionId)},[]),[f,p]=(0,u.useState)({status:`connecting`,errorMsg:null,url:null,title:null,viewport:{width:1280,height:800},hoverRect:null,hoverSelector:null,hoverPath:null,popup:null});(0,u.useEffect)(()=>{if(!r||!t)return;let e=!1,u=null;p(e=>({...e,status:`connecting`,errorMsg:null,popup:null}));let d=R({decode:e=>createImageBitmap(new Blob([e],{type:`image/jpeg`})),draw:e=>{let t=a.current;t&&(t.width!==e.width&&(t.width=e.width),t.height!==e.height&&(t.height=e.height),t.getContext(`2d`)?.drawImage(e,0,0))},release:e=>e.close()}),f=B()?H(()=>d.stats()):null;return(async()=>{try{let r=await v(t,i);if(e){O(t,r.id);return}u=r.id,s.current={id:r.id,projectId:t},p(e=>({...e,url:r.url,title:r.title,viewport:{width:r.viewportWidth,height:r.viewportHeight}}));let a=A(r.id,t);o.current=a,a.onmessage=e=>{if(typeof e.data==`string`){try{let t=JSON.parse(e.data);if(t.type===`ready`)p(e=>({...e,status:`ready`,url:t.url??e.url,title:t.title??e.title,viewport:t.viewport??e.viewport}));else if(t.type===`nav`)p(e=>({...e,url:t.url??e.url,title:t.title??e.title}));else if(t.type===`hover`)p(e=>({...e,hoverRect:t.rect??null,hoverSelector:t.selector??null,hoverPath:t.path??null}));else if(t.type===`popup`){let e=typeof t.count==`number`?t.count:0;p(n=>({...n,popup:e>0?{count:e,active:t.active===!0,url:t.url??null}:null}))}}catch{}return}d.push(e.data)},a.onopen=()=>{e||p(e=>({...e,status:`ready`}))},a.onerror=()=>{e||p(e=>({...e,status:`error`,errorMsg:n.t(`browser:session.connectionError`)}))},a.onclose=()=>{e||p(e=>e.status===`ready`?{...e,status:`error`,errorMsg:n.t(`browser:session.lostConnection`)}:e)}}catch(t){if(e)return;let r=t instanceof h?n.t(`browser:session.tooManySessions`):t instanceof g?n.t(`browser:session.launchFailed`):n.t(`browser:session.openFailed`);p(e=>({...e,status:`error`,errorMsg:r}))}})(),()=>{e=!0,f?.(),d.dispose();let n=o.current;if(n){n.onmessage=null,n.onopen=null,n.onerror=null,n.onclose=null;try{n.close()}catch{}}o.current=null;let r=u;r&&s.current?.id===r&&s.current.projectId===t&&(s.current=null),r&&(c.current>0?l.current.set(`${t}\0${r}`,{sessionId:r,projectId:t}):O(t,r))}},[r,t,i,d]);let m=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`input`,event:e}))}catch{}},[]),_=(0,u.useCallback)(async(e,t)=>{let n=s.current;if(n)try{let r=await y(n.projectId,n.id,e,t);if(s.current?.id!==n.id||s.current.projectId!==n.projectId)return;p(e=>({...e,url:r.url,title:r.title}))}catch{}},[]),x=(0,u.useCallback)(async(e,t,n)=>{let r=s.current;if(!r)throw Error(`No active browser session`);c.current+=1;try{return await b(r.projectId,r.id,e,t,n)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),C=(0,u.useCallback)(async(e,t,n,r)=>{let i=s.current;if(!i)throw Error(`No active browser session`);c.current+=1;try{return await S(i.projectId,i.id,e,t,n,r)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),D=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?w(n.projectId,n.id,e,t):{text:``}},[]),k=(0,u.useCallback)((e,t)=>{let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t));p(e=>({...e,viewport:{width:n,height:r}})),m({type:`resize`,width:n,height:r})},[m]),j=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`probe`,x:e.x,y:e.y}))}catch{}},[]),M=(0,u.useCallback)(()=>{p(e=>e.hoverRect||e.hoverPath?{...e,hoverRect:null,hoverSelector:null,hoverPath:null}:e)},[]),N=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?T(n.projectId,n.id,e,t):null},[]),P=(0,u.useCallback)(e=>{let t=s.current;t&&(p(t=>t.popup?{...t,popup:{...t.popup,active:e===`popup`}}:t),E(t.projectId,t.id,e))},[]);return{...f,canvasRef:a,forwardInput:m,navigate:_,capture:x,captureBreakpoints:C,clipboard:D,navigateElement:N,setViewport:k,probe:j,clearHover:M,setPopupView:P}}export{L as a,M as c,P as d,C as f,s as g,c as h,j as i,N as l,l as m,m as n,p as o,f as p,I as r,F as s,U as t,D as u};
|
|
1
|
+
import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t}from"./jsx-runtime-zu2_FqZY.js";import{o as n}from"./i18n-cK5pPbj8.js";import{t as r}from"./createLucideIcon-Bjbbp_KS.js";import{a as i,n as a}from"./auth-D_5bQv5W.js";import{t as o}from"./api-B2M7CtDw.js";var s=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),c=r(`globe`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]]),l=r(`rotate-cw`,[[`path`,{d:`M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8`,key:`1p45f6`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}]]),u=e(t(),1);function d(){return typeof window<`u`&&`__TAURI_INTERNALS__`in window||typeof window<`u`&&window.location.protocol!==`http:`&&window.location.protocol!==`https:`?`ws://localhost:4200`:`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`}var f=d();function p(){let e={BASE_URL:`/`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1}.VITE_FEATURE_BROWSER_CAPTURE;return typeof e!=`string`||e!==`false`}var m={desktop:{width:1280,height:800},tablet:{width:768,height:1024},mobile:{width:375,height:667}},h=class extends Error{},g=class extends Error{};function _(e){if(!e)throw Error(`A project owner is required for browser sessions`);return`${i}/api/projects/${encodeURIComponent(e)}`}async function v(e,t){let n=await fetch(`${_(e)}/browser/sessions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({initialUrl:t})});if(n.status===409)throw new h(`Too many open browser sessions`);if(n.status===502)throw new g(`The browser failed to launch`);if(!n.ok)throw Error(`Failed to open browser (${n.status})`);return(await n.json()).session}async function y(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/navigate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,url:r})});if(!i.ok)throw Error(`Navigation failed (${i.status})`);return await i.json()}async function b(e,t,n,r,i){let a=await fetch(`${_(e)}/browser/sessions/${t}/capture`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,pendingSpecId:r,captureNetwork:i?.captureNetwork??!0})});if(!a.ok)throw Error(await x(a));return await a.json()}async function x(e){let t=await e.json().then(e=>e.error).catch(()=>void 0);return t?`Capture failed (${e.status}): ${t}`:`Capture failed (${e.status})`}async function S(e,t,n,r,i,a=m){let o=await fetch(`${_(e)}/browser/sessions/${t}/capture-breakpoints`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,anchorPoint:r,pendingSpecId:i,breakpoints:a})});if(!o.ok)throw Error(await x(o));return await o.json()}async function C(e,t,n){let r=new FormData;r.append(`file`,t,n);let i=await fetch(`${o()}/tickets/${e}/attachments`,{method:`POST`,body:r});if(!i.ok)throw Error(`Upload failed (${i.status})`);return(await i.json()).attachment}async function w(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/clipboard`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,text:r})});if(!i.ok)throw Error(`Clipboard failed (${i.status})`);return await i.json()}async function T(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/element`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({selector:n,direction:r})});if(!i.ok)throw Error(`Element navigate failed (${i.status})`);return(await i.json()).probe}async function E(e,t,n){try{await fetch(`${_(e)}/browser/sessions/${t}/popup-view`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({target:n})})}catch{}}function D(e){if(!e||e===`about:blank`)return`…`;try{return new URL(e).hostname||`…`}catch{return`…`}}async function O(e,t){try{await fetch(`${_(e)}/browser/sessions/${t}`,{method:`DELETE`})}catch{}}function k(e,t){return`${f||(typeof window<`u`?`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`:`ws://localhost:4200`)}/ws/browser/${e}?projectId=${encodeURIComponent(t)}`}function A(e,t){let n=k(e,t),r=a(),i=r?new WebSocket(n,[`specrails-desktop`,r]):new WebSocket(n);return i.binaryType=`arraybuffer`,i}function j(e,t=e=>requestAnimationFrame(e),n=e=>cancelAnimationFrame(e)){let r=null,i=null,a=null,o=!1,s=()=>{a!=null&&(n(a),a=null);let t=r,o=i;r=null,i=null,t&&(!o||t.x!==o.x||t.y!==o.y)&&e({type:`mouse`,action:`move`,x:t.x,y:t.y}),o&&e({type:`wheel`,x:o.x,y:o.y,deltaX:o.deltaX,deltaY:o.deltaY})},c=()=>{a==null&&!o&&(a=t(()=>{a=null,s()}))};return{move(e,t){o||(r={x:e,y:t},c())},wheel(e,t,n,r){o||(i?(i.x=e,i.y=t,i.deltaX+=n,i.deltaY+=r):i={x:e,y:t,deltaX:n,deltaY:r},c())},flush(){o||s()},dispose(){o||(o=!0,a!=null&&(n(a),a=null),r=null,i=null)}}}function M(e,t,n){let r=t.width>0?(e.x-t.left)/t.width:0,i=t.height>0?(e.y-t.top)/t.height:0,a=Math.min(Math.max(r,0),1)*n.width,o=Math.min(Math.max(i,0),1)*n.height;return{x:Math.round(a),y:Math.round(o)}}function N(e,t,n){let r=n.width>0?t.width/n.width:0,i=n.height>0?t.height/n.height:0;return{left:t.left+e.x*r,top:t.top+e.y*i,width:e.width*r,height:e.height*i}}function P(e,t){return{x:Math.min(e.x,t.x),y:Math.min(e.y,t.y),width:Math.abs(e.x-t.x),height:Math.abs(e.y-t.y)}}function F(e,t=8){return e.width>=t&&e.height>=t}function I(e,t){let n=Math.max(1,t.width),r=Math.max(1,t.height),i=Math.max(0,Math.min(e.x,n-1)),a=Math.max(0,Math.min(e.y,r-1)),o=Math.min(n,Math.max(i+1,e.x+e.width)),s=Math.min(r,Math.max(a+1,e.y+e.height));return{x:Math.round(i),y:Math.round(a),width:Math.round(o-i),height:Math.round(s-a)}}function L(e){return{nodeCount:e.nodes.length,htmlBytes:e.html.length,truncated:e.htmlTruncated,networkCount:e.networkRequests?.length??0}}function R(e){let t=e.schedule??(e=>requestAnimationFrame(e)),n=e.cancel??(e=>cancelAnimationFrame(e)),r=e.now??(()=>typeof performance<`u`?performance.now():Date.now()),i=e.release??(()=>{}),a={received:0,decoded:0,drawn:0,droppedStale:0,droppedUndrawn:0,decodeMsTotal:0,drawMsTotal:0},o=!1,s=!1,c=null,l=null,u=null,d=()=>{u=null;let t=l;if(l=null,t==null||o){t!=null&&f(t);return}let n=r();try{e.draw(t),a.drawn++,a.drawMsTotal+=r()-n}catch{}finally{f(t)}},f=e=>{try{i(e)}catch{}},p=()=>{u==null&&!o&&(u=t(d))},m=t=>{s=!0;let n=r();e.decode(t).then(e=>{if(o){f(e);return}a.decoded++,a.decodeMsTotal+=r()-n,l!=null&&(a.droppedUndrawn++,f(l)),l=e,p(),h()},()=>{h()})},h=()=>{if(o){s=!1;return}let e=c;c=null,e==null?s=!1:m(e)};return{push(e){if(!o){if(a.received++,s){c!=null&&a.droppedStale++,c=e;return}m(e)}},stats(){return{...a}},dispose(){if(!o){if(o=!0,c=null,u!=null){try{n(u)}catch{}u=null}l!=null&&(f(l),l=null)}}}}var z=`specrails-desktop:browser-capture-debug`;function B(){try{return typeof localStorage<`u`&&localStorage.getItem(z)===`1`}catch{return!1}}function V(e,t,n){let r=Math.max(1,n),i=t.decoded-e.decoded,a=t.drawn-e.drawn,o=e=>Math.round(e*10)/10;return{fpsReceived:o((t.received-e.received)*1e3/r),fpsDrawn:o(a*1e3/r),droppedStale:t.droppedStale-e.droppedStale,droppedUndrawn:t.droppedUndrawn-e.droppedUndrawn,avgDecodeMs:i>0?o((t.decodeMsTotal-e.decodeMsTotal)/i):0,avgDrawMs:a>0?o((t.drawMsTotal-e.drawMsTotal)/a):0}}function H(e,t=e=>console.table([e]),n=2e3,r=()=>Date.now()){let i=e(),a=r(),o=setInterval(()=>{let n=e(),o=r();t(V(i,n,o-a)),i=n,a=o},n);return()=>clearInterval(o)}function U(e){let{projectId:t,open:r,initialUrl:i}=e,a=(0,u.useRef)(null),o=(0,u.useRef)(null),s=(0,u.useRef)(null),c=(0,u.useRef)(0),l=(0,u.useRef)(new Map),d=(0,u.useCallback)(()=>{if(c.current!==0||l.current.size===0)return;let e=[...l.current.values()];l.current.clear();for(let t of e)O(t.projectId,t.sessionId)},[]),[f,p]=(0,u.useState)({status:`connecting`,errorMsg:null,url:null,title:null,viewport:{width:1280,height:800},hoverRect:null,hoverSelector:null,hoverPath:null,popup:null});(0,u.useEffect)(()=>{if(!r||!t)return;let e=!1,u=null;p(e=>({...e,status:`connecting`,errorMsg:null,popup:null}));let d=R({decode:e=>createImageBitmap(new Blob([e],{type:`image/jpeg`})),draw:e=>{let t=a.current;t&&(t.width!==e.width&&(t.width=e.width),t.height!==e.height&&(t.height=e.height),t.getContext(`2d`)?.drawImage(e,0,0))},release:e=>e.close()}),f=B()?H(()=>d.stats()):null;return(async()=>{try{let r=await v(t,i);if(e){O(t,r.id);return}u=r.id,s.current={id:r.id,projectId:t},p(e=>({...e,url:r.url,title:r.title,viewport:{width:r.viewportWidth,height:r.viewportHeight}}));let a=A(r.id,t);o.current=a,a.onmessage=e=>{if(typeof e.data==`string`){try{let t=JSON.parse(e.data);if(t.type===`ready`)p(e=>({...e,status:`ready`,url:t.url??e.url,title:t.title??e.title,viewport:t.viewport??e.viewport}));else if(t.type===`nav`)p(e=>({...e,url:t.url??e.url,title:t.title??e.title}));else if(t.type===`hover`)p(e=>({...e,hoverRect:t.rect??null,hoverSelector:t.selector??null,hoverPath:t.path??null}));else if(t.type===`popup`){let e=typeof t.count==`number`?t.count:0;p(n=>({...n,popup:e>0?{count:e,active:t.active===!0,url:t.url??null}:null}))}}catch{}return}d.push(e.data)},a.onopen=()=>{e||p(e=>({...e,status:`ready`}))},a.onerror=()=>{e||p(e=>({...e,status:`error`,errorMsg:n.t(`browser:session.connectionError`)}))},a.onclose=()=>{e||p(e=>e.status===`ready`?{...e,status:`error`,errorMsg:n.t(`browser:session.lostConnection`)}:e)}}catch(t){if(e)return;let r=t instanceof h?n.t(`browser:session.tooManySessions`):t instanceof g?n.t(`browser:session.launchFailed`):n.t(`browser:session.openFailed`);p(e=>({...e,status:`error`,errorMsg:r}))}})(),()=>{e=!0,f?.(),d.dispose();let n=o.current;if(n){n.onmessage=null,n.onopen=null,n.onerror=null,n.onclose=null;try{n.close()}catch{}}o.current=null;let r=u;r&&s.current?.id===r&&s.current.projectId===t&&(s.current=null),r&&(c.current>0?l.current.set(`${t}\0${r}`,{sessionId:r,projectId:t}):O(t,r))}},[r,t,i,d]);let m=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`input`,event:e}))}catch{}},[]),_=(0,u.useCallback)(async(e,t)=>{let n=s.current;if(n)try{let r=await y(n.projectId,n.id,e,t);if(s.current?.id!==n.id||s.current.projectId!==n.projectId)return;p(e=>({...e,url:r.url,title:r.title}))}catch{}},[]),x=(0,u.useCallback)(async(e,t,n)=>{let r=s.current;if(!r)throw Error(`No active browser session`);c.current+=1;try{return await b(r.projectId,r.id,e,t,n)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),C=(0,u.useCallback)(async(e,t,n,r)=>{let i=s.current;if(!i)throw Error(`No active browser session`);c.current+=1;try{return await S(i.projectId,i.id,e,t,n,r)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),D=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?w(n.projectId,n.id,e,t):{text:``}},[]),k=(0,u.useCallback)((e,t)=>{let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t));p(e=>({...e,viewport:{width:n,height:r}})),m({type:`resize`,width:n,height:r})},[m]),j=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`probe`,x:e.x,y:e.y}))}catch{}},[]),M=(0,u.useCallback)(()=>{p(e=>e.hoverRect||e.hoverPath?{...e,hoverRect:null,hoverSelector:null,hoverPath:null}:e)},[]),N=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?T(n.projectId,n.id,e,t):null},[]),P=(0,u.useCallback)(e=>{let t=s.current;t&&(p(t=>t.popup?{...t,popup:{...t.popup,active:e===`popup`}}:t),E(t.projectId,t.id,e))},[]);return{...f,canvasRef:a,forwardInput:m,navigate:_,capture:x,captureBreakpoints:C,clipboard:D,navigateElement:N,setViewport:k,probe:j,clearHover:M,setPopupView:P}}export{L as a,M as c,P as d,C as f,s as g,c as h,j as i,N as l,l as m,m as n,p as o,f as p,I as r,F as s,U as t,D as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-zu2_FqZY.js";import{o as r}from"./i18n-
|
|
1
|
+
import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-zu2_FqZY.js";import{o as r}from"./i18n-cK5pPbj8.js";import{n as i}from"./dist-bVqfYmby.js";import{a}from"./auth-D_5bQv5W.js";import{n as o}from"./useSharedWebSocket-DaPh7QBL.js";import{n as s}from"./api-B2M7CtDw.js";var c=e(t(),1),l=new Map;function u(e,t){return`${e}:${t}`}function d(e){return(e instanceof Error?e.message:e==null?``:String(e)).trim()||`Request failed`}function f(e){if(!e)return;let t=`${e}:`;for(let e of l.keys())e.startsWith(t)&&l.delete(e)}function p({namespace:e,projectId:t,initialValue:n,fetcher:r,pollInterval:i=0}){let a=t?u(t,e):null,[o,s]=(0,c.useState)(()=>a&&l.has(a)?l.get(a):n),[f,p]=(0,c.useState)(()=>!a||!l.has(a)),[m,h]=(0,c.useState)(f),[g,_]=(0,c.useState)(null),v=(0,c.useRef)(r);v.current=r;let y=(0,c.useRef)(n);y.current=n;let b=(0,c.useRef)(a);b.current=a;let x=(0,c.useRef)(0),S=(0,c.useRef)(null),C=(0,c.useCallback)(async(e,t,n)=>{let r=++x.current;S.current?.abort();let i=new AbortController;S.current=i,n&&b.current===e&&(h(!0),_(null));try{let n=await v.current({projectId:t,signal:i.signal});if(i.signal.aborted||r!==x.current)return;l.set(e,n),b.current===e&&(s(n),_(null))}catch(t){if(i.signal.aborted||r!==x.current)return;b.current===e&&_(d(t))}finally{r===x.current&&(S.current===i&&(S.current=null),b.current===e&&(h(!1),p(!1)))}},[]);return(0,c.useEffect)(()=>{if(x.current+=1,S.current?.abort(),S.current=null,_(null),!a||!t){s(y.current),p(!0),h(!1);return}let e=l.has(a);e?(s(l.get(a)),p(!1),h(!1)):(s(y.current),p(!0),h(!0)),C(a,t,!e);let n;return i>0&&(n=setInterval(()=>{C(a,t,!1)},i)),()=>{x.current+=1,S.current?.abort(),S.current=null,n&&clearInterval(n)}},[a,t,i,C]),{data:o,isLoading:m,isFirstLoad:f,error:g,refresh:(0,c.useCallback)(async()=>{!a||!t||await C(a,t,!0)},[a,t,C])}}var m=n();function h(e){return e.providers&&e.providers.length>0?e.providers:[e.provider]}var g=(0,c.createContext)(null),_=`specrails-desktop:activeProjectId`;function v(e){try{e?localStorage.setItem(_,e):localStorage.removeItem(_)}catch{}}function y(){try{return localStorage.getItem(_)}catch{return null}}function b({children:e}){let[t,n]=(0,c.useState)([]),[l,u]=(0,c.useState)(null),[d,p]=(0,c.useState)(!0),[h,_]=(0,c.useState)(!1),[b,x]=(0,c.useState)(new Set),S=(0,c.useRef)(null),C=(0,c.useRef)(new Set),w=(0,c.useCallback)(e=>{v(e),s(e),u(t=>(t!==null&&t!==e&&(S.current&&clearTimeout(S.current),_(!0),S.current=setTimeout(()=>_(!1),400)),e))},[]),{registerHandler:T,unregisterHandler:E}=o();(0,c.useEffect)(()=>{async function e(){try{let e=await fetch(`${a}/api/projects`);if(!e.ok)return;let t=await e.json();n(t.projects),t.setupProjectIds&&t.setupProjectIds.length>0&&x(new Set(t.setupProjectIds))}catch{}finally{p(!1)}}e()},[]);let D=(0,c.useCallback)(e=>{let t=e;if(typeof t.type==`string`){if(t.type===`desktop.projects`){if(!Array.isArray(t.projects))return;let e=t.projects;n(e),u(t=>{let n;if(t&&e.find(e=>e.id===t))n=t;else{let t=y();n=t&&e.find(e=>e.id===t)?t:e.length>0?e[0].id:null}return v(n),s(n),n}),p(!1)}else if(t.type===`desktop.project_added`){let e=t.project;n(t=>t.find(t=>t.id===e.id)?t:[...t,e]),C.current.has(e.id)?C.current.delete(e.id):(i.success(r.t(`nav:projects.added`,{name:e.name})),w(e.id))}else if(t.type===`desktop.project_removed`){let e=t.projectId;i.success(r.t(`nav:projects.removed`)),f(e),n(t=>t.filter(t=>t.id!==e)),u(t=>t===e?(v(null),s(null),null):t)}}},[]);(0,c.useLayoutEffect)(()=>(T(`desktop`,D),()=>E(`desktop`)),[D,T,E]);let O=(0,c.useCallback)(async(e,t,r=[`claude`])=>{try{let i={path:e,providers:r.length>0?r:[`claude`]};t&&(i.name=t);let a=await fetch(`/api/projects`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i)});if(!a.ok){let e=await a.json();throw Error(e.error??`HTTP ${a.status}`)}let o=await a.json();return C.current.add(o.project.id),n(e=>e.find(e=>e.id===o.project.id)?e:[...e,o.project]),w(o.project.id),o}catch(e){throw console.error(`[useDesktop] addProject error:`,e),e}},[w]),k=(0,c.useCallback)(async e=>{try{let t=await fetch(`${a}/api/projects/${e}`,{method:`DELETE`});if(!t.ok){let e=await t.json();throw Error(e.error??`HTTP ${t.status}`)}f(e),n(t=>t.filter(t=>t.id!==e)),x(t=>{if(!t.has(e))return t;let n=new Set(t);return n.delete(e),n}),u(t=>t===e?(v(null),s(null),null):t)}catch(e){throw console.error(`[useDesktop] removeProject error:`,e),e}},[]),A=(0,c.useCallback)(e=>{x(t=>new Set([...t,e]))},[]),j=(0,c.useCallback)(e=>{x(t=>{let n=new Set(t);return n.delete(e),n})},[]),M=(0,c.useMemo)(()=>({projects:t,activeProjectId:l,setActiveProjectId:w,addProject:O,removeProject:k,isLoading:d,isSwitchingProject:h,setupProjectIds:b,startSetupWizard:A,completeSetupWizard:j}),[t,l,w,O,k,d,h,b,A,j]);return(0,m.jsx)(g.Provider,{value:M,children:e})}var x={projects:[],activeProjectId:null,setActiveProjectId:()=>{},addProject:async()=>null,removeProject:async()=>{},isLoading:!1,isSwitchingProject:!1,setupProjectIds:new Set,startSetupWizard:()=>{},completeSetupWizard:()=>{}};function S(){return(0,c.useContext)(g)??x}export{p as i,h as n,S as r,b as t};
|
package/client/dist/index.html
CHANGED
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
.specrails-splash__mark .pill { opacity: 1; }
|
|
205
205
|
}
|
|
206
206
|
</style>
|
|
207
|
-
<script type="module" crossorigin src="/assets/index-
|
|
207
|
+
<script type="module" crossorigin src="/assets/index-CCq_kIrF.js"></script>
|
|
208
208
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
|
|
209
209
|
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
|
|
210
210
|
<link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
<link rel="modulepreload" crossorigin href="/assets/code-BDXEM4ml.js">
|
|
225
225
|
<link rel="modulepreload" crossorigin href="/assets/commands-DyUZQ1FI.js">
|
|
226
226
|
<link rel="modulepreload" crossorigin href="/assets/common-DHsv9N5d.js">
|
|
227
|
-
<link rel="modulepreload" crossorigin href="/assets/dashboard-
|
|
227
|
+
<link rel="modulepreload" crossorigin href="/assets/dashboard-Cslu_GCH.js">
|
|
228
228
|
<link rel="modulepreload" crossorigin href="/assets/explore-DgBACn39.js">
|
|
229
229
|
<link rel="modulepreload" crossorigin href="/assets/integrations-mB-7zoyT.js">
|
|
230
230
|
<link rel="modulepreload" crossorigin href="/assets/jira-CmKWERE0.js">
|
|
@@ -237,19 +237,19 @@
|
|
|
237
237
|
<link rel="modulepreload" crossorigin href="/assets/specs-DII0C8gw.js">
|
|
238
238
|
<link rel="modulepreload" crossorigin href="/assets/terminal-BFRZnMPP.js">
|
|
239
239
|
<link rel="modulepreload" crossorigin href="/assets/tickets-D_yDquEw.js">
|
|
240
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
241
|
-
<link rel="modulepreload" crossorigin href="/assets/Trans-
|
|
240
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-cK5pPbj8.js">
|
|
241
|
+
<link rel="modulepreload" crossorigin href="/assets/Trans-Cm9rhdnG.js">
|
|
242
242
|
<link rel="modulepreload" crossorigin href="/assets/dist-bVqfYmby.js">
|
|
243
243
|
<link rel="modulepreload" crossorigin href="/assets/arrow-left-CpIR4j7G.js">
|
|
244
244
|
<link rel="modulepreload" crossorigin href="/assets/auth-D_5bQv5W.js">
|
|
245
245
|
<link rel="modulepreload" crossorigin href="/assets/api-B2M7CtDw.js">
|
|
246
|
-
<link rel="modulepreload" crossorigin href="/assets/useBrowserCaptureSession-
|
|
246
|
+
<link rel="modulepreload" crossorigin href="/assets/useBrowserCaptureSession-DR1_jL56.js">
|
|
247
247
|
<link rel="modulepreload" crossorigin href="/assets/arrow-up-right-9DoMn7fD.js">
|
|
248
248
|
<link rel="modulepreload" crossorigin href="/assets/sparkles-DrY8hb0X.js">
|
|
249
249
|
<link rel="modulepreload" crossorigin href="/assets/x-CBMI9ejn.js">
|
|
250
250
|
<link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-DaPh7QBL.js">
|
|
251
|
-
<link rel="modulepreload" crossorigin href="/assets/useDesktop-
|
|
252
|
-
<link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext
|
|
251
|
+
<link rel="modulepreload" crossorigin href="/assets/useDesktop-D4DxBGPU.js">
|
|
252
|
+
<link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-DJAkRYps.js">
|
|
253
253
|
<link rel="modulepreload" crossorigin href="/assets/external-link-BcU5EWZp.js">
|
|
254
254
|
<link rel="modulepreload" crossorigin href="/assets/chevron-right-CViMxucY.js">
|
|
255
255
|
<link rel="modulepreload" crossorigin href="/assets/circle-x-CMYsN4ZH.js">
|
|
@@ -272,11 +272,11 @@
|
|
|
272
272
|
<link rel="modulepreload" crossorigin href="/assets/dialog-CzIekCw0.js">
|
|
273
273
|
<link rel="modulepreload" crossorigin href="/assets/jira-api-BgFIr8eD.js">
|
|
274
274
|
<link rel="modulepreload" crossorigin href="/assets/lib-CGdyxIWg.js">
|
|
275
|
-
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-
|
|
276
|
-
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-
|
|
277
|
-
<link rel="modulepreload" crossorigin href="/assets/spending-
|
|
278
|
-
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-
|
|
279
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
275
|
+
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-DwzZXHtg.js">
|
|
276
|
+
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-BAXZRbEO.js">
|
|
277
|
+
<link rel="modulepreload" crossorigin href="/assets/spending-BLb5pj6G.js">
|
|
278
|
+
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-CFh7GOv5.js">
|
|
279
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BT6xDQjB.css">
|
|
280
280
|
</head>
|
|
281
281
|
<body>
|
|
282
282
|
<div id="specrails-splash" aria-hidden="false" role="status" aria-label="Loading Specrails">
|
|
@@ -118,9 +118,14 @@ actionable.
|
|
|
118
118
|
verification distinguish changed, resumed, and no-change results.
|
|
119
119
|
- Per-unit settlement returns structured execution + delivery results. `onLoopRunFinished` receives
|
|
120
120
|
the engine outcome only; commit/status/ref/provenance/push failures cannot rewrite it.
|
|
121
|
-
- Clean, committed worktrees may be released only after a final live preflight proves tracked
|
|
122
|
-
untracked
|
|
123
|
-
|
|
121
|
+
- Clean, committed worktrees may be released only after a final live preflight proves tracked and
|
|
122
|
+
untracked status (apart from durably recorded Specrails overlay paths), plus exact worktree HEAD
|
|
123
|
+
and branch ref. Gitignored paths are release-safe only when covered by the IMMUTABLE settlement
|
|
124
|
+
snapshot (`DeliverBranchRecord.settlementIgnoredPaths`, captured the moment the worktree was
|
|
125
|
+
proven clean) — run-created build caches (`__pycache__`, `node_modules`) release cleanly, while an
|
|
126
|
+
ignored path that APPEARS after settlement, a failed/oversized capture (>400 paths → null), a
|
|
127
|
+
legacy row without a snapshot, or conflicting per-branch snapshots all preserve the worktree
|
|
128
|
+
(`durableSettlementIgnoredPaths`, cleanup-and-checkout-tolerate-run-artifacts). Authenticated overlay roots are atomically renamed to a unique
|
|
124
129
|
persistent same-filesystem quarantine batch and revalidated there; automatic cleanup never unlinks that
|
|
125
130
|
quarantine, so a raced replacement or open-descriptor write remains recoverable. The batch root is
|
|
126
131
|
persisted before the first move and pointers are never evicted while their bytes remain on disk. A recreated
|
|
@@ -439,6 +444,30 @@ is superseded atomically and run on that PR head. A stale
|
|
|
439
444
|
concurrent request receives 409 before it can allocate or claim anything. Legacy / non-isolated
|
|
440
445
|
launches are unaffected.
|
|
441
446
|
|
|
447
|
+
## Explicit target PR (deliver-rail-into-existing-pr)
|
|
448
|
+
|
|
449
|
+
The launch body accepts an optional `targetPrNumber` — the user's explicit designation of an
|
|
450
|
+
existing open PR as the delivery destination ("extend PR #151"). It bypasses the automatic-inference
|
|
451
|
+
status gates (which never probe GitHub for a `todo` ticket) but keeps the authoritative validation
|
|
452
|
+
ladder: `resolveExplicitPrTarget` (`server/active-pr-continuation.ts`) resolves the number to its
|
|
453
|
+
canonical URL, observes the lifecycle, and throws `ExplicitPrTargetError` with a distinct code —
|
|
454
|
+
`target_pr_not_found` (404) / `target_pr_not_open` / `target_pr_fork` (`isCrossRepository !== false`)
|
|
455
|
+
/ `target_pr_invalid` / `target_pr_unfetchable` (all 409) — rejecting the launch BEFORE any delivery
|
|
456
|
+
row or worktree exists; there is never a silent fallback to a fresh integration-branch run. The
|
|
457
|
+
router also 400s `invalid_target_pr` (shape) and `target_pr_requires_pr_mode` (loops/PR-delivery off,
|
|
458
|
+
non-isolated launch, or isolation unavailable). A valid target becomes one `ActivePrContinuationTarget`
|
|
459
|
+
(`source: 'explicit-target'`) applied to every launch ticket — the existing single-checkout collapse,
|
|
460
|
+
born-attached generation (`pr_url`/`pr_number` at insert, `isContinuation`, `borrowed-pr` ownership),
|
|
461
|
+
and attached-PR decision card all reuse the automatic-continuation machinery byte-identically. The
|
|
462
|
+
PR's `baseRefName` wins over the configured integration branch. When the slot has a continuable
|
|
463
|
+
delivery for the SAME PR the continuation contract drives instead (explicit target dropped as
|
|
464
|
+
redundant); a DIFFERENT number 409s `pr_decision_pending`. Surfaces: dashboard rail header
|
|
465
|
+
`RailTargetPrSelector` (candidates from `GET /rails/:i/pr-candidates` — the automatic matchers
|
|
466
|
+
without the status gate, display-only, fork rows disabled — plus manual number entry; selection is
|
|
467
|
+
one-shot, consumed by the launch) with `TargetPrLaunchDialog` confirming number/title/head branch;
|
|
468
|
+
MCP `specrails_rails(launch, targetPrNumber)` with operator-prompt teaching ("user names a PR →
|
|
469
|
+
pass it; never create a duplicate"). Fork PR push support and multi-PR batches remain out of scope.
|
|
470
|
+
|
|
442
471
|
## Ticket lifecycle — `on_review`
|
|
443
472
|
|
|
444
473
|
New status in `TicketStatus` / `VALID_STATUSES` (`server/ticket-store.ts`), between
|
package/package.json
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ExplicitPrTargetError = void 0;
|
|
37
|
+
exports.resolveExplicitPrTarget = resolveExplicitPrTarget;
|
|
38
|
+
exports.listPrCandidatesForTickets = listPrCandidatesForTickets;
|
|
36
39
|
exports.resolveActivePrContinuationTargets = resolveActivePrContinuationTargets;
|
|
37
40
|
const jira_db_1 = require("./jira/jira-db");
|
|
38
41
|
const rail_pr_store_1 = require("./rail-pr-store");
|
|
@@ -40,6 +43,15 @@ const integration_branch_1 = require("./integration-branch");
|
|
|
40
43
|
const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
41
44
|
const pr_lifecycle_1 = require("./pr-lifecycle");
|
|
42
45
|
const fs = __importStar(require("fs"));
|
|
46
|
+
class ExplicitPrTargetError extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'ExplicitPrTargetError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.ExplicitPrTargetError = ExplicitPrTargetError;
|
|
43
55
|
const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
44
56
|
const PR_MATCH_RANK = {
|
|
45
57
|
'jira-key': 1,
|
|
@@ -145,6 +157,68 @@ async function materializeTarget(git, repoDir, fetchOk, ticketId, branch, baseBr
|
|
|
145
157
|
return { ticketId, deliveryId, branch: head, baseBranch: base, baseRef: remoteRef, prUrl, prNumber, isDraft, deliverySha, source };
|
|
146
158
|
return null;
|
|
147
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve an EXPLICITLY user-designated PR number into a continuation target.
|
|
162
|
+
* This is the user's answer, not a discovery guess: it bypasses the automatic
|
|
163
|
+
* inference gates (`canProbeGithubForContinuation`, spec-text matching) but
|
|
164
|
+
* keeps the same authoritative validation ladder — the PR must be verifiably
|
|
165
|
+
* OPEN on the SAME repository with a materializable head. Every failure throws
|
|
166
|
+
* `ExplicitPrTargetError` with a distinct code; callers reject the launch
|
|
167
|
+
* before any delivery row or worktree exists.
|
|
168
|
+
*/
|
|
169
|
+
async function resolveExplicitPrTarget(input) {
|
|
170
|
+
const { prNumber } = input;
|
|
171
|
+
// 1. Resolve the PR number to its canonical URL (identity for the delivery
|
|
172
|
+
// row and for every later lifecycle observation / poll-merge).
|
|
173
|
+
let lookup;
|
|
174
|
+
try {
|
|
175
|
+
lookup = await input.exec.run('gh', ['pr', 'view', String(prNumber), '--json', 'url,number'], input.repoDir);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
throw new ExplicitPrTargetError('target_pr_not_found', `PR #${prNumber} could not be looked up: ${(err instanceof Error ? err.message : String(err)).slice(0, 256)}`);
|
|
179
|
+
}
|
|
180
|
+
if (lookup.code !== 0) {
|
|
181
|
+
const detail = (lookup.stderr.trim() || lookup.stdout.trim()).split('\n')[0].slice(0, 256);
|
|
182
|
+
throw new ExplicitPrTargetError('target_pr_not_found', `PR #${prNumber} could not be looked up: ${detail || `gh exited ${lookup.code}`}`);
|
|
183
|
+
}
|
|
184
|
+
let prUrl;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(lookup.stdout);
|
|
187
|
+
if (typeof parsed.url !== 'string' || !parsed.url.trim() || parsed.number !== prNumber) {
|
|
188
|
+
throw new Error('unexpected gh payload');
|
|
189
|
+
}
|
|
190
|
+
prUrl = parsed.url.trim();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new ExplicitPrTargetError('target_pr_invalid', `GitHub returned an unusable identity for PR #${prNumber}`);
|
|
194
|
+
}
|
|
195
|
+
// 2. Authoritative lifecycle observation — state, fork boundary, exact head.
|
|
196
|
+
const observed = await (0, pr_lifecycle_1.observePrLifecycle)(input.exec, input.repoDir, prUrl);
|
|
197
|
+
if (!observed.ok) {
|
|
198
|
+
throw new ExplicitPrTargetError('target_pr_not_found', `PR #${prNumber} lifecycle could not be confirmed: ${observed.detail}`);
|
|
199
|
+
}
|
|
200
|
+
if (observed.state !== 'OPEN') {
|
|
201
|
+
throw new ExplicitPrTargetError('target_pr_not_open', `PR #${prNumber} is ${observed.state.toLowerCase()}, not open`);
|
|
202
|
+
}
|
|
203
|
+
// `null` means GitHub did not prove the head belongs to this repository —
|
|
204
|
+
// conservative rejection: a fork head is not pushable with origin rights.
|
|
205
|
+
if (observed.isCrossRepository !== false) {
|
|
206
|
+
throw new ExplicitPrTargetError('target_pr_fork', `PR #${prNumber} head lives on a fork (or same-repo could not be proven); fork PRs are not pushable`);
|
|
207
|
+
}
|
|
208
|
+
const head = observed.headRefName?.trim();
|
|
209
|
+
const base = observed.baseRefName?.trim() || input.integrationBranch;
|
|
210
|
+
if (!head || !base || head === base || !(0, integration_branch_1.isValidBranchName)(head) || !(0, integration_branch_1.isValidBranchName)(base) || !observed.headRefOid) {
|
|
211
|
+
throw new ExplicitPrTargetError('target_pr_invalid', `PR #${prNumber} head/base refs are unusable (head=${head ?? '?'}, base=${base ?? '?'})`);
|
|
212
|
+
}
|
|
213
|
+
// 3. Materialize exactly like automatic continuations: the head branch must
|
|
214
|
+
// exist locally or be fetchable from origin; the worktree verification
|
|
215
|
+
// later pins it to the observed head OID.
|
|
216
|
+
const target = await materializeTarget(input.git, input.repoDir, input.fetchOk, 0, head, base, prUrl, prNumber, observed.isDraft, observed.headRefOid, null, 'explicit-target');
|
|
217
|
+
if (!target) {
|
|
218
|
+
throw new ExplicitPrTargetError('target_pr_unfetchable', `PR #${prNumber} head branch ${head} is neither present locally nor fetchable from origin`);
|
|
219
|
+
}
|
|
220
|
+
return target;
|
|
221
|
+
}
|
|
148
222
|
function hasRetainedOrUnsettledWorktree(db, worktreeIds) {
|
|
149
223
|
return worktreeIds.some((id) => {
|
|
150
224
|
const worktree = (0, rail_worktrees_store_1.getRailWorktree)(db, id);
|
|
@@ -268,7 +342,7 @@ async function materializeHistoricalContinuation(input, candidate, isDraft) {
|
|
|
268
342
|
source: 'rail-pr-delivery',
|
|
269
343
|
}]));
|
|
270
344
|
}
|
|
271
|
-
const PR_JSON_FIELDS = 'number,title,body,headRefName,baseRefName,url,isDraft,state';
|
|
345
|
+
const PR_JSON_FIELDS = 'number,title,body,headRefName,baseRefName,url,isDraft,state,isCrossRepository';
|
|
272
346
|
async function listOpenPrs(exec, repoDir) {
|
|
273
347
|
const r = await exec.run('gh', ['pr', 'list', '--state', 'open', '--limit', '200', '--json', PR_JSON_FIELDS], repoDir);
|
|
274
348
|
if (r.code !== 0)
|
|
@@ -319,6 +393,46 @@ function mergeOpenPrs(prs) {
|
|
|
319
393
|
}
|
|
320
394
|
return out;
|
|
321
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Display-only candidate discovery for the launch UI's "deliver into existing
|
|
398
|
+
* PR" picker. Reuses the same matchers as automatic continuation (PR-number
|
|
399
|
+
* mention in the spec text, Jira key in the PR title/body/head) but WITHOUT
|
|
400
|
+
* the ticket-status gate — suggestions are informational and never
|
|
401
|
+
* auto-selected, so the strict gates guarding automatic adoption don't apply.
|
|
402
|
+
* Matched PRs sort first; the remaining open PRs follow for manual pick.
|
|
403
|
+
*/
|
|
404
|
+
async function listPrCandidatesForTickets(input) {
|
|
405
|
+
const mentioned = new Set();
|
|
406
|
+
for (const ticketId of input.ticketIds) {
|
|
407
|
+
for (const n of mentionedPrNumbers(input.getTicketSpec(ticketId)))
|
|
408
|
+
mentioned.add(n);
|
|
409
|
+
}
|
|
410
|
+
const openPrs = mergeOpenPrs([
|
|
411
|
+
...await listOpenPrs(input.exec, input.repoDir),
|
|
412
|
+
...await mentionedOpenPrs(input.exec, input.repoDir, mentioned),
|
|
413
|
+
]);
|
|
414
|
+
const matched = [];
|
|
415
|
+
const rest = [];
|
|
416
|
+
for (const pr of openPrs) {
|
|
417
|
+
const number = typeof pr.number === 'number' ? pr.number : parsePrNumber(pr.url);
|
|
418
|
+
const url = pr.url?.trim();
|
|
419
|
+
const head = pr.headRefName?.trim();
|
|
420
|
+
if (!number || !url || !head)
|
|
421
|
+
continue;
|
|
422
|
+
const candidate = {
|
|
423
|
+
number,
|
|
424
|
+
title: pr.title?.trim() || `PR #${number}`,
|
|
425
|
+
headRefName: head,
|
|
426
|
+
baseRefName: pr.baseRefName?.trim() || '',
|
|
427
|
+
url,
|
|
428
|
+
isDraft: pr.isDraft === true,
|
|
429
|
+
isCrossRepository: pr.isCrossRepository !== false,
|
|
430
|
+
};
|
|
431
|
+
const isMatch = input.ticketIds.some((ticketId) => (prMatchKind(input.db, ticketId, input.getTicketSpec(ticketId), pr) !== null));
|
|
432
|
+
(isMatch ? matched : rest).push(candidate);
|
|
433
|
+
}
|
|
434
|
+
return [...matched, ...rest];
|
|
435
|
+
}
|
|
322
436
|
/**
|
|
323
437
|
* Detects tickets whose next implementation pass should continue an already
|
|
324
438
|
* open PR branch instead of creating fresh work from the integration branch.
|
|
@@ -396,6 +396,15 @@ summary later.
|
|
|
396
396
|
is explicit, covering Jira projects whose Review status has not been mapped to
|
|
397
397
|
Specrails \`on_review\`. You do NOT need to know or pass the branch name. If
|
|
398
398
|
there is no confident open PR match, the normal new-work flow is preserved.
|
|
399
|
+
- **Deliver into an existing PR (explicit target).** When the user names an
|
|
400
|
+
existing PR as the destination ("extend PR #151", "push this into the open
|
|
401
|
+
PR"), pass \`targetPrNumber\` on \`specrails_rails(launch)\` — NEVER launch
|
|
402
|
+
without it in that case, because a plain launch creates a DUPLICATE PR. The
|
|
403
|
+
rail then works on that PR's head branch and settle pushes to it. The PR must
|
|
404
|
+
be open and same-repo; the launch fails closed with \`target_pr_not_found\` /
|
|
405
|
+
\`target_pr_not_open\` / \`target_pr_fork\` / \`target_pr_invalid\` /
|
|
406
|
+
\`target_pr_unfetchable\` — report the reason, do not retry with a fresh
|
|
407
|
+
launch unless the user explicitly chooses a new PR.
|
|
399
408
|
- **PR-aware relaunch classification.** Before classifying or relaunching a
|
|
400
409
|
follow-up for an \`on_review\` spec or active PR, inspect that PR's head
|
|
401
410
|
branch/diff/files, not only \`main\`. Treat OpenSpec artifacts added or
|
|
@@ -120,6 +120,12 @@ function railsTools() {
|
|
|
120
120
|
.enum(['low', 'medium', 'high'])
|
|
121
121
|
.optional()
|
|
122
122
|
.describe('Reasoning-effort tier for loop launches (launch, loop mode). From the in-app agent chat, omitting it defaults to the launching conversation\'s effort when that is low/medium/high.'),
|
|
123
|
+
targetPrNumber: zod_1.z
|
|
124
|
+
.number()
|
|
125
|
+
.int()
|
|
126
|
+
.min(1)
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Deliver INTO an existing open PR instead of creating a new one (launch). When the user names an existing PR ("extend PR #151"), pass its number here — the rail then works on that PR\'s head branch and settle pushes to it; NEVER launch without it in that case (a plain launch would create a duplicate PR). The PR must be open and same-repo (fork PRs are rejected with target_pr_fork).'),
|
|
123
129
|
},
|
|
124
130
|
async handler(ctx, args) {
|
|
125
131
|
const base = `${(0, types_1.projectPath)(ctx, args.projectId)}/rails`;
|
|
@@ -206,6 +212,8 @@ function railsTools() {
|
|
|
206
212
|
body.loopId = args.loopId;
|
|
207
213
|
if (args.reasoning_effort !== undefined)
|
|
208
214
|
body.reasoning_effort = args.reasoning_effort;
|
|
215
|
+
if (args.targetPrNumber !== undefined)
|
|
216
|
+
body.targetPrNumber = args.targetPrNumber;
|
|
209
217
|
// Origin link (safe-pr-review-flow): a launch driven by the in-app
|
|
210
218
|
// agent carries its conversation id (from the loopback header, via
|
|
211
219
|
// ctx) so the PR decision card is posted back into that conversation.
|
|
@@ -64,7 +64,11 @@ async function listLocalBranches(repoDir) {
|
|
|
64
64
|
* clean checkout. */
|
|
65
65
|
async function inspectProjectCheckoutCleanliness(repoDir) {
|
|
66
66
|
try {
|
|
67
|
-
|
|
67
|
+
// Tracked modifications only. `git checkout` never loses untracked files —
|
|
68
|
+
// app-owned state (`.specrails/local-tickets.json`) or editor droppings
|
|
69
|
+
// must not permanently block Checkout; a genuine untracked collision still
|
|
70
|
+
// aborts inside git itself and surfaces as a failed checkout.
|
|
71
|
+
const status = await git(repoDir, ['status', '--porcelain', '--untracked-files=no']);
|
|
68
72
|
return { ok: true, clean: status === '' };
|
|
69
73
|
}
|
|
70
74
|
catch (err) {
|
|
@@ -190,7 +194,8 @@ async function checkoutProjectReviewBranch(repoDir, branch, expectedSha) {
|
|
|
190
194
|
}
|
|
191
195
|
let dirty = false;
|
|
192
196
|
try {
|
|
193
|
-
|
|
197
|
+
// Tracked modifications only — see inspectProjectCheckoutCleanliness.
|
|
198
|
+
dirty = (await git(repoDir, ['status', '--porcelain', '--untracked-files=no'])) !== '';
|
|
194
199
|
}
|
|
195
200
|
catch (err) {
|
|
196
201
|
return { ok: false, error: err instanceof Error ? err.message : 'git status failed' };
|
|
@@ -249,6 +249,41 @@ function aggregateImplementationOutcome(results) {
|
|
|
249
249
|
return 'partially_succeeded';
|
|
250
250
|
return results.length > 0 ? 'failed' : 'unknown';
|
|
251
251
|
}
|
|
252
|
+
const SETTLEMENT_IGNORED_SNAPSHOT_CAP = 400;
|
|
253
|
+
/** Capture the gitignored paths present at the moment the worktree was proven
|
|
254
|
+
* clean — the IMMUTABLE settlement snapshot that later authorizes releasing
|
|
255
|
+
* run-created ignored artifacts (build caches like __pycache__/node_modules).
|
|
256
|
+
* Fail-safe: any git failure, malformed line, or overflow records null (no
|
|
257
|
+
* ignored authorization → release preserves exactly as before). */
|
|
258
|
+
async function captureSettlementIgnoredPaths(git, worktreePath, overlayExcludes) {
|
|
259
|
+
try {
|
|
260
|
+
const status = await git.run([
|
|
261
|
+
'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
|
|
262
|
+
...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
|
|
263
|
+
], worktreePath);
|
|
264
|
+
if (status.code !== 0)
|
|
265
|
+
return null;
|
|
266
|
+
const paths = [];
|
|
267
|
+
for (const raw of status.stdout.split('\n')) {
|
|
268
|
+
const line = raw.trimEnd();
|
|
269
|
+
if (!line)
|
|
270
|
+
continue;
|
|
271
|
+
// Non-ignored entries are the dirty check's jurisdiction, not the snapshot's.
|
|
272
|
+
if (!line.startsWith('!! '))
|
|
273
|
+
continue;
|
|
274
|
+
const entry = line.slice(3).replace(/\/$/, '');
|
|
275
|
+
if (!entry)
|
|
276
|
+
return null;
|
|
277
|
+
paths.push(entry);
|
|
278
|
+
if (paths.length > SETTLEMENT_IGNORED_SNAPSHOT_CAP)
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
return paths;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
252
287
|
function branchRecords(results) {
|
|
253
288
|
return results.flatMap((result) => result.run.ticketIds.map((ticketId) => ({
|
|
254
289
|
ticketId,
|
|
@@ -266,6 +301,7 @@ function branchRecords(results) {
|
|
|
266
301
|
worktreePath: result.run.handle.worktreePath,
|
|
267
302
|
overlayExcludes: result.run.overlayExcludes,
|
|
268
303
|
overlayCleanupEvidence: result.run.overlayCleanupEvidence,
|
|
304
|
+
settlementIgnoredPaths: result.run.settlementIgnoredPaths ?? null,
|
|
269
305
|
})));
|
|
270
306
|
}
|
|
271
307
|
/**
|
|
@@ -451,26 +487,43 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
451
487
|
// rail delivery row can represent one PR URL, so multi-PR batches continue to
|
|
452
488
|
// use the existing new-work flow instead of silently mixing PRs.
|
|
453
489
|
const continuationTargets = prMode
|
|
454
|
-
?
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
490
|
+
? input.explicitPrTarget
|
|
491
|
+
// Explicit designation is the user's answer, not a discovery guess: it
|
|
492
|
+
// replaces automatic resolution entirely and applies the ONE verified
|
|
493
|
+
// target to every launch ticket. Validation failure throws
|
|
494
|
+
// ExplicitPrTargetError here — before createPrDeliveryGeneration — so a
|
|
495
|
+
// rejected launch leaves zero rows, branches, or worktrees behind.
|
|
496
|
+
? await (async () => {
|
|
497
|
+
const target = await (0, active_pr_continuation_1.resolveExplicitPrTarget)({
|
|
498
|
+
git,
|
|
499
|
+
exec,
|
|
500
|
+
repoDir: baseRepo,
|
|
501
|
+
prNumber: input.explicitPrTarget.prNumber,
|
|
502
|
+
integrationBranch: integration.branch,
|
|
503
|
+
fetchOk: fetchResult.ok,
|
|
504
|
+
});
|
|
505
|
+
return new Map(ticketIds.map((ticketId) => [ticketId, { ...target, ticketId }]));
|
|
506
|
+
})()
|
|
507
|
+
: await (0, active_pr_continuation_1.resolveActivePrContinuationTargets)({
|
|
508
|
+
db: ctx.db,
|
|
509
|
+
git,
|
|
510
|
+
exec,
|
|
511
|
+
repoDir: baseRepo,
|
|
512
|
+
// Continuation authority is scoped to the launch's exact durable ticket
|
|
513
|
+
// set. In scope=all there is only one isolation unit, but its primary
|
|
514
|
+
// ticket must never stand in for the full batch during PR discovery.
|
|
515
|
+
ticketIds: [...ticketIds],
|
|
516
|
+
integrationBranch: integration.branch,
|
|
517
|
+
fetchOk: fetchResult.ok,
|
|
518
|
+
getTicketSpec: (ticketId) => {
|
|
519
|
+
try {
|
|
520
|
+
return ctx.getTicketSpec(ticketId);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
})
|
|
474
527
|
: new Map();
|
|
475
528
|
const uniqueContinuationKeys = new Set(ticketIds
|
|
476
529
|
.map((ticketId) => continuationTargets.get(ticketId))
|
|
@@ -820,6 +873,11 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
820
873
|
safeToRelease: false,
|
|
821
874
|
};
|
|
822
875
|
}
|
|
876
|
+
// The worktree is proven clean THIS instant — freeze the ignored-path set
|
|
877
|
+
// as the settlement snapshot. Everything ignored right now is run residue
|
|
878
|
+
// by construction (the worktree is app-created and deliverables are
|
|
879
|
+
// committed); anything ignored that appears LATER preserves the worktree.
|
|
880
|
+
a.settlementIgnoredPaths = await captureSettlementIgnoredPaths(git, a.handle.worktreePath, a.overlayExcludes);
|
|
823
881
|
let finalSha = null;
|
|
824
882
|
let verificationFailure;
|
|
825
883
|
if (a.continuationTarget) {
|
|
@@ -1263,6 +1321,7 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
1263
1321
|
const settledBranchRecords = branchRecords(results);
|
|
1264
1322
|
const expectedHeadByBranch = (0, rail_worktree_release_1.durableBranchHeads)(settledBranchRecords);
|
|
1265
1323
|
const overlayEvidenceByBranch = (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(settledBranchRecords);
|
|
1324
|
+
const settlementIgnoredByBranch = (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(settledBranchRecords);
|
|
1266
1325
|
const patch = {
|
|
1267
1326
|
branches: settledBranchRecords,
|
|
1268
1327
|
worktreeIds,
|
|
@@ -1298,11 +1357,11 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
1298
1357
|
const cleanupWarnings = [
|
|
1299
1358
|
...await (0, rail_worktree_release_1.releaseRailWorktrees)({
|
|
1300
1359
|
db: ctx.db, git, repoDir: baseRepo, worktreeIds: failedSafeIds,
|
|
1301
|
-
state: 'failed', remove, expectedHeadByBranch, overlayEvidenceByBranch, onSafetyArchive,
|
|
1360
|
+
state: 'failed', remove, expectedHeadByBranch, overlayEvidenceByBranch, settlementIgnoredByBranch, onSafetyArchive,
|
|
1302
1361
|
}),
|
|
1303
1362
|
...await (0, rail_worktree_release_1.releaseRailWorktrees)({
|
|
1304
1363
|
db: ctx.db, git, repoDir: baseRepo, worktreeIds: completedSafeIds,
|
|
1305
|
-
remove, expectedHeadByBranch, overlayEvidenceByBranch, onSafetyArchive,
|
|
1364
|
+
remove, expectedHeadByBranch, overlayEvidenceByBranch, settlementIgnoredByBranch, onSafetyArchive,
|
|
1306
1365
|
}),
|
|
1307
1366
|
];
|
|
1308
1367
|
if (cleanupWarnings.length > 0) {
|
|
@@ -1479,7 +1538,7 @@ async function inspectRecoveryWorktree(git, repoDir, row) {
|
|
|
1479
1538
|
const [status, actualBranch, head, branchRef] = await Promise.all([
|
|
1480
1539
|
git.run([
|
|
1481
1540
|
'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
|
|
1482
|
-
...worktree_manager_1.
|
|
1541
|
+
...worktree_manager_1.PR_NEVER_STAGE_PATHSPEC_ROOTS.map((entry) => `:(exclude)${entry}`),
|
|
1483
1542
|
...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
|
|
1484
1543
|
], row.worktree_path),
|
|
1485
1544
|
git.run(['rev-parse', '--abbrev-ref', 'HEAD'], row.worktree_path),
|