specrails-desktop 2.29.2 → 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-ClpD5CFj.js → ActivityFeedPage-CDMzpGXd.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-BVTacapZ.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-D9bhFyuy.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-DAjN8Au5.js} +1 -1
- package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-CebykajE.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BiPrVvgv.js} +1 -1
- package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Dw9eQFRS.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-cCG_JAEi.js} +1 -1
- package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-C1kZ_mRP.js} +1 -1
- package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-3GivxWJm.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-KuZXGdr8.js} +1 -1
- package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DWpRALkB.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-mrXDKafj.js} +1 -1
- package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-CUYXjcoc.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-CwjBD5Gm.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-CO9ton0-.js} +1 -1
- package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-CLL5UuvN.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext-DJAkRYps.js} +1 -1
- package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-DZvCC5Ri.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-C7Hesadg.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-DczCzUzP.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-CFh7GOv5.js} +4 -4
- package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-Cm9rhdnG.js} +1 -1
- package/client/dist/assets/agent-BXc59ARK2.js +1 -0
- package/client/dist/assets/agent-BbIFXdDq.js +1 -0
- package/client/dist/assets/agent-BqEfA5u5.js +1 -0
- package/client/dist/assets/agent-BtGzgTC4.js +1 -0
- package/client/dist/assets/agent-CIhys_5d.js +1 -0
- package/client/dist/assets/agent-CJPaqsVR.js +1 -0
- package/client/dist/assets/agent-n-hY4GuL.js +1 -0
- package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
- package/client/dist/assets/common-6MLbD-jM.js +1 -0
- package/client/dist/assets/common-BKGEu4rm.js +1 -0
- package/client/dist/assets/common-BNbnv8cE2.js +1 -0
- package/client/dist/assets/common-BqGWtDbt.js +1 -0
- package/client/dist/assets/common-DBtTzeYm2.js +1 -0
- package/client/dist/assets/common-DEW3Nr8n.js +1 -0
- package/client/dist/assets/{common-DVQuhs-m.js → common-DHsv9N5d.js} +1 -1
- package/client/dist/assets/common-mkrEcW-Y.js +1 -0
- package/client/dist/assets/dashboard-CHCEkds4.js +1 -0
- package/client/dist/assets/dashboard-Cqmmbh3l2.js +1 -0
- package/client/dist/assets/dashboard-CrlTV1ms.js +1 -0
- package/client/dist/assets/dashboard-Cslu_GCH.js +1 -0
- package/client/dist/assets/dashboard-DUU8-_Rg.js +1 -0
- package/client/dist/assets/dashboard-DweT3Y4R.js +1 -0
- package/client/dist/assets/dashboard-Dy2Um-US2.js +1 -0
- package/client/dist/assets/dashboard-WhzOQw1S.js +1 -0
- package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-BERxOT0X.js} +1 -1
- package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-Di8lflPo.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-BAXZRbEO.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DwzZXHtg.js} +1 -1
- package/client/dist/assets/{i18n-CgaRxpII.js → i18n-cK5pPbj8.js} +2 -2
- package/client/dist/assets/index-BT6xDQjB.css +2 -0
- package/client/dist/assets/index-CCq_kIrF.js +76 -0
- package/client/dist/assets/{spending-C4p06nen.js → spending-BLb5pj6G.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-DR1_jL56.js} +1 -1
- package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-D4DxBGPU.js} +1 -1
- package/client/dist/index.html +14 -14
- package/docs/internals/safe-pr-review-flow.md +250 -38
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +352 -53
- package/server/dist/agent-chat-manager.js +29 -12
- package/server/dist/agent-operator-prompt.js +9 -0
- package/server/dist/agent-store.js +19 -4
- package/server/dist/db.js +21 -0
- package/server/dist/mcp/tools/rails.js +8 -0
- package/server/dist/pr-lifecycle.js +100 -8
- package/server/dist/project-git.js +68 -3
- package/server/dist/rail-isolated-launch.js +719 -124
- package/server/dist/rail-pr-decision.js +1073 -181
- package/server/dist/rail-pr-recovery-git.js +235 -0
- package/server/dist/rail-pr-store.js +120 -9
- package/server/dist/rail-worktree-release.js +376 -4
- package/server/dist/rails-router.js +196 -10
- package/server/dist/worktree-manager.js +149 -18
- package/server/dist/worktree-overlay.js +166 -4
- package/client/dist/assets/agent-4rmXoo_p.js +0 -1
- package/client/dist/assets/agent-BxZuNGih.js +0 -1
- package/client/dist/assets/agent-C3uXOYv_.js +0 -1
- package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
- package/client/dist/assets/agent-CASKrhG12.js +0 -1
- package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
- package/client/dist/assets/agent-DitUxYh2.js +0 -1
- package/client/dist/assets/agent-Doow4X7o.js +0 -1
- package/client/dist/assets/common-CD1QHriA2.js +0 -1
- package/client/dist/assets/common-CwwPaRxx.js +0 -1
- package/client/dist/assets/common-Dg2jaX7d.js +0 -1
- package/client/dist/assets/common-DuBwj6tK.js +0 -1
- package/client/dist/assets/common-MRj9MHcc.js +0 -1
- package/client/dist/assets/common-O4ZpnMLp.js +0 -1
- package/client/dist/assets/common-fyeF0v7C2.js +0 -1
- package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
- package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
- package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
- package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
- package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
- package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
- package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
- package/client/dist/assets/dashboard-owcDucDi.js +0 -1
- package/client/dist/assets/index-BflYQpWm.css +0 -2
- package/client/dist/assets/index-C1_-BTXy.js +0 -72
|
@@ -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-CgaRxpII.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">
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-Bjbbp_KS.js">
|
|
214
214
|
<link rel="modulepreload" crossorigin href="/assets/activity-DiyjsgQV.js">
|
|
215
215
|
<link rel="modulepreload" crossorigin href="/assets/addspec-Rw8uVBC1.js">
|
|
216
|
-
<link rel="modulepreload" crossorigin href="/assets/agent-
|
|
216
|
+
<link rel="modulepreload" crossorigin href="/assets/agent-BqEfA5u5.js">
|
|
217
217
|
<link rel="modulepreload" crossorigin href="/assets/agents-DhG7mg6-.js">
|
|
218
218
|
<link rel="modulepreload" crossorigin href="/assets/agentstudio-BIUe1Im2.js">
|
|
219
219
|
<link rel="modulepreload" crossorigin href="/assets/aiedit-iIjz2_Qk.js">
|
|
@@ -223,8 +223,8 @@
|
|
|
223
223
|
<link rel="modulepreload" crossorigin href="/assets/chat-BGD7MXAy.js">
|
|
224
224
|
<link rel="modulepreload" crossorigin href="/assets/code-BDXEM4ml.js">
|
|
225
225
|
<link rel="modulepreload" crossorigin href="/assets/commands-DyUZQ1FI.js">
|
|
226
|
-
<link rel="modulepreload" crossorigin href="/assets/common-
|
|
227
|
-
<link rel="modulepreload" crossorigin href="/assets/dashboard-
|
|
226
|
+
<link rel="modulepreload" crossorigin href="/assets/common-DHsv9N5d.js">
|
|
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">
|
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
|
|
10
10
|
## The single source of truth
|
|
11
11
|
|
|
12
|
-
`rail_pr_deliveries` (per-project `jobs.sqlite`, migrations 36/38/48–
|
|
12
|
+
`rail_pr_deliveries` (per-project `jobs.sqlite`, migrations 36/38/48–54; CRUD in
|
|
13
13
|
`server/rail-pr-store.ts`) stores one durable **generation** per implementation launch. A partial
|
|
14
14
|
unique index enforces one non-terminal generation per rail. Reimplementing an attached PR creates
|
|
15
|
-
generation B and atomically moves generation A to terminal `superseded`; A
|
|
16
|
-
B closes.
|
|
15
|
+
generation B and atomically moves generation A to terminal `superseded`; A cannot reappear through
|
|
16
|
+
ordinary replay after B closes. The sole exception is an allocation rollback committed in the same
|
|
17
|
+
transaction: B becomes terminal and restored A carries `restored_from_delivery_id=B` as exact causal
|
|
18
|
+
proof. `supersedes_delivery_id` preserves forward lineage and `is_continuation` records that the
|
|
17
19
|
PR/head branch is borrowed review state rather than a resource this generation owns.
|
|
18
20
|
|
|
21
|
+
Every projected snapshot also carries `createdAt`/`updatedAt`; claim, transition, release, recovery,
|
|
22
|
+
and supersession writes advance the latter with a strictly monotonic millisecond UTC logical clock.
|
|
23
|
+
Consumers replace a different generation only through an explicit supersedes link or a strictly
|
|
24
|
+
later valid `createdAt`, and advance the same id only with newer update evidence under a deterministic fail-closed tie policy.
|
|
25
|
+
Delivery ids already observed terminal or superseded are tombstoned against non-terminal replay.
|
|
26
|
+
Only the exact paired rollback above may consume such a tombstone; a delayed predecessor without
|
|
27
|
+
that durable proof remains rejected. Missing or ambiguous ordering evidence fails closed instead of
|
|
28
|
+
letting arrival order resurrect an old card.
|
|
29
|
+
|
|
19
30
|
The important rule is that no single enum carries all truth. The row has orthogonal axes:
|
|
20
31
|
|
|
21
32
|
- `implementation_outcome`: `running | succeeded | partially_succeeded | failed | unknown` —
|
|
@@ -27,6 +38,10 @@ The important rule is that no single enum carries all truth. The row has orthogo
|
|
|
27
38
|
`merged`, `discarded`, `superseded`).
|
|
28
39
|
- `status_code` + bounded `status_detail`: localized stage/reason and secondary diagnostics.
|
|
29
40
|
- `delivery_sha`: the exact verified object used by an existing-PR push or retry.
|
|
41
|
+
- `supersedes_delivery_id` / `restored_from_delivery_id`: forward replacement and exact failed-
|
|
42
|
+
replacement rollback lineage.
|
|
43
|
+
- `safety_archives`: deduplicated roots for persistent overlay quarantine batches; shown as neutral recovery
|
|
44
|
+
evidence rather than cleanup failures.
|
|
30
45
|
- `pr_state`: `none | local-only | pushed | pr-created`, independent of all three axes.
|
|
31
46
|
|
|
32
47
|
`branches` is the durable per-unit record: ticket/run, branch, actual implementation result,
|
|
@@ -44,7 +59,10 @@ engine succeeded + no branch delta → no_changes
|
|
|
44
59
|
mixed clean units → on_review / partial (explicit subset)
|
|
45
60
|
engine succeeded + commit/ref unsafe → pr_failed / blocked (worktree preserved)
|
|
46
61
|
verified existing-PR push failed → pr_failed / retryable_failure (exact SHA retained)
|
|
47
|
-
|
|
62
|
+
exact OPEN head/base contains SHA → attached delivery verified
|
|
63
|
+
exact CLOSED head/base contains SHA → pr_closed
|
|
64
|
+
exact MERGED in a decision action → merged immediately (terminal)
|
|
65
|
+
stale/retargeted/terminal missing SHA → on_review, PR detached, SHA retained
|
|
48
66
|
new generation replaces prior one → prior generation superseded (terminal)
|
|
49
67
|
fresh no-change accepted → completed (terminal; no merge claimed)
|
|
50
68
|
```
|
|
@@ -54,10 +72,30 @@ truthfully with “delivery needs attention”. A fresh no-op never offers Creat
|
|
|
54
72
|
no-op leaves the existing PR untouched. Partial cards name the included and failed units before
|
|
55
73
|
delivery.
|
|
56
74
|
|
|
57
|
-
Continuation cleanup is ownership-aware: **Dismiss** clears the follow-up card/worktree
|
|
75
|
+
Continuation cleanup is ownership-aware: **Dismiss** clears the follow-up card/worktree when its
|
|
76
|
+
final live preflight proves that removal is lossless, but never
|
|
58
77
|
closes the pre-existing PR, deletes its head branch, or returns its tickets from review. A blocked
|
|
59
|
-
dirty follow-up requires an explicit
|
|
60
|
-
|
|
78
|
+
dirty follow-up requires an explicit “Discard local result” confirmation, while still preserving
|
|
79
|
+
the external PR/branch. That confirmation terminalizes the workflow result; it does not authorize
|
|
80
|
+
forced byte deletion. New tracked, untracked, or ignored files keep the worktree and its checked-out
|
|
81
|
+
branch mounted with an actionable cleanup warning. If this computer has no authenticated live
|
|
82
|
+
worktree path, the blocked continuation shows ownership-safe **Dismiss** instead of claiming that a
|
|
83
|
+
local result will be discarded; the existing PR/branch/review state remains untouched. Fresh deliveries keep the full PR/ticket discard
|
|
84
|
+
semantics under the same lossless local preflight.
|
|
85
|
+
|
|
86
|
+
When a terminal dashboard action removes the card while recovery data remains, the cleanup warning
|
|
87
|
+
stays visible indefinitely with the retained location and a localized Copy recovery details action.
|
|
88
|
+
Agent Chat keeps the same evidence in terminal history.
|
|
89
|
+
|
|
90
|
+
A dismissed continuation remains a strict historical anchor for the next local-only iteration. With
|
|
91
|
+
no active delivery, the resolver considers only the newest terminal generation that overlaps any
|
|
92
|
+
requested ticket. Reuse requires that row itself to be a discarded continuation for the exact full
|
|
93
|
+
ticket set, with one valid PR/head/base/SHA; GitHub must still report that exact PR `OPEN` at that SHA,
|
|
94
|
+
every available ref must agree, and no prior worktree may remain mounted or unsettled. A newer subset,
|
|
95
|
+
superset, fresh, malformed, merged, completed, or superseded generation shadows older history. Failed
|
|
96
|
+
historical proof blocks Jira/PR heuristics instead of silently selecting a different review. The next
|
|
97
|
+
generation stores the verified baseline SHA and terminal predecessor lineage before worktree
|
|
98
|
+
allocation so a crash is retryable and same-timestamp clients still elect the replacement.
|
|
61
99
|
|
|
62
100
|
Fresh no-change cards offer explicit **Mark done** and **Refine** outcomes. Mark done uses terminal
|
|
63
101
|
`completed` and moves review tickets to Done without claiming a merge; Refine explicitly returns
|
|
@@ -74,19 +112,69 @@ actionable.
|
|
|
74
112
|
- `prMode` is captured once. Admission is rechecked inside the per-repository allocation lock;
|
|
75
113
|
two requests cannot both create a generation or reuse the ticket-keyed worktree.
|
|
76
114
|
- A continuation supersedes its prior generation and creates the new `building` row in one DB
|
|
77
|
-
transaction. Allocation failure closes the new row and restores the prior generation atomically
|
|
115
|
+
transaction. Allocation failure closes the new row and restores the prior generation atomically,
|
|
116
|
+
recording the failed replacement id so every client can safely undo its tombstone for that pair.
|
|
78
117
|
- Isolation units remain `per-ticket` or one `all`-scope batch unit. Initial/final SHAs plus commit
|
|
79
118
|
verification distinguish changed, resumed, and no-change results.
|
|
80
119
|
- Per-unit settlement returns structured execution + delivery results. `onLoopRunFinished` receives
|
|
81
120
|
the engine outcome only; commit/status/ref/provenance/push failures cannot rewrite it.
|
|
82
|
-
- Clean, committed worktrees may be released
|
|
83
|
-
|
|
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
|
|
129
|
+
persistent same-filesystem quarantine batch and revalidated there; automatic cleanup never unlinks that
|
|
130
|
+
quarantine, so a raced replacement or open-descriptor write remains recoverable. The batch root is
|
|
131
|
+
persisted before the first move and pointers are never evicted while their bytes remain on disk. A recreated
|
|
132
|
+
original path makes final non-force removal fail closed. Each quarantine batch root is persisted and shown
|
|
133
|
+
in a collapsed card section without changing an otherwise successful delivery to
|
|
134
|
+
`cleanup_incomplete`. If a terminal dashboard action creates an archive, a persistent notification
|
|
135
|
+
exposes its exact paths and a Copy paths action before the card is removed. Dirty, unknown,
|
|
136
|
+
ignored-user-data, concurrent-write, or ref-
|
|
137
|
+
mismatched worktrees become retryable `needs-review`; push failure retains the exact SHA.
|
|
84
138
|
- Project admission remains closed while startup reconciliation runs under the repo lock. Every
|
|
85
139
|
stale `building` shape becomes actionable; successful interrupted work is preserved and labelled
|
|
86
140
|
`settlement_interrupted`, while only all-failed runs become `implementation_failed`.
|
|
87
141
|
- Operation leases are process capabilities. Startup clears every prior-process lease before card
|
|
88
142
|
projection, marks the still-active delivery `operation_interrupted`, and leaves its durable SHA,
|
|
89
143
|
PR and unit evidence intact so the user can safely retry.
|
|
144
|
+
- Migrated successful `settlement_interrupted` and `recovery_unavailable` rows with an existing PR
|
|
145
|
+
are re-examined at startup. Startup and the explicit recovery action share one bounded,
|
|
146
|
+
fail-closed scan over refs, reflogs, and unreachable objects; a command/read failure, malformed
|
|
147
|
+
output, excessive combined candidate set, or ambiguous marker can never become false proof of a
|
|
148
|
+
unique result. Every refs/reflogs, unreachable-object, or recorded-branch candidate must carry the unique `(run <id>)` settlement
|
|
149
|
+
marker in its **commit subject**; branch/worktree association or a marker found only in the body
|
|
150
|
+
is never causal proof. The unreachable-object scan runs at most once per serialized reconciliation,
|
|
151
|
+
accepts only a bounded, unambiguous set of commit objects, and fails closed on malformed or excessive
|
|
152
|
+
output. A terminal `released`/legacy `failed` branch tip is only a fallback under
|
|
153
|
+
that same subject rule. One unambiguous run-owned commit becomes `delivery_sha` and a Retry push action
|
|
154
|
+
when absent from the recorded open PR. If GitHub already exposes that exact causal commit, the
|
|
155
|
+
row is classified as delivered without a no-op push. Startup audits earlier legacy recoveries
|
|
156
|
+
that already reached draft/ready too, replacing a frozen old PR-head SHA with the run-owned
|
|
157
|
+
commit when needed. Existing-PR recovery repairs `is_continuation=true` before exposing actions;
|
|
158
|
+
per-unit `branchOwnership=borrowed-pr|preexisting` independently prevents branch deletion without
|
|
159
|
+
incorrectly making every fresh replacement PR a borrowed continuation.
|
|
160
|
+
Before persisting or pushing a recovered orphan, Specrails pins it under a delivery-specific
|
|
161
|
+
`refs/specrails/recovery/*` ref. The ref survives crashes and protects the object from Git garbage
|
|
162
|
+
collection until exact remote delivery is proved or the user explicitly discards the result.
|
|
163
|
+
Discovery still runs when a ledger path exists but is dirty, recreated, or unauthenticated; if a
|
|
164
|
+
distinct worktree result and run-owned orphan coexist, both are preserved and neither is selected,
|
|
165
|
+
bundled, or pushed automatically. Restart preserves a `recover-and-retry` recovery-family status,
|
|
166
|
+
and explicit discard may remove a pre-persistence ref only after its exact run/final ownership is
|
|
167
|
+
re-proven. If the ref was advanced from marker-bearing ancestor A to descendant B before a crash,
|
|
168
|
+
that exact delivery ref selects B rather than letting the global scan manufacture self-ambiguity.
|
|
169
|
+
Successful successor delivery and startup settlement retry clean up exact ancestor refs.
|
|
170
|
+
A later unrelated branch tip is never substituted for the durable final SHA or unique run-marked
|
|
171
|
+
object, and all matching units are updated together. Dirty, missing, mismatched, unmarked or
|
|
172
|
+
ambiguous evidence stays blocked and preserved. Once the exact causal SHA is proven, a transient
|
|
173
|
+
GitHub lookup instead stays retryable and Retry push revalidates before mutation.
|
|
174
|
+
- A blocked legacy card exposes a worktree path only after the path is proved absolute,
|
|
175
|
+
non-symlink, outside the main checkout, uniquely present in this repository's live worktree
|
|
176
|
+
registry, and checked out on the recorded branch/HEAD. Historical or other-device paths are
|
|
177
|
+
removed from the snapshot instead of being presented as available.
|
|
90
178
|
- `GET /rails` is hydration-only and never invokes crash recovery. This avoids misclassifying the
|
|
91
179
|
normal live window between durable loop completion and commit/ref/push settlement.
|
|
92
180
|
- Startup retries pending terminal ticket effects in-process and re-projects active and terminal
|
|
@@ -116,12 +204,17 @@ entries. Each allocated worktree therefore gets `applyWorktreeOverlay(...)` at l
|
|
|
116
204
|
`worktrees/` entry are never linked (nested pipeline worktrees stay local). `.mcp.json`
|
|
117
205
|
and the instruction file (CLAUDE.md/AGENTS.md/GEMINI.md per provider) are COPIED when
|
|
118
206
|
the checkout lacks them. Windows: junction → dereferencing-copy fallback.
|
|
119
|
-
- **Never on the PR**: every
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
207
|
+
- **Never on the PR**: every allocator-created overlay path is excluded from staging with a literal
|
|
208
|
+
top-level pathspec. The writable worktree manifest is not cleanup authority; settlement persists
|
|
209
|
+
fingerprints captured from the trusted source/created entries. Cleanup atomically renames each
|
|
210
|
+
authenticated root into a unique sibling quarantine, revalidates it after the move, discloses
|
|
211
|
+
raced content there, and never deletes or restores quarantined paths automatically over a
|
|
212
|
+
possibly recreated source. A modified or
|
|
213
|
+
concurrently extended overlay is preserved as user data, while a no-op
|
|
214
|
+
overlay keeps the byte-identical ordinary staging behavior.
|
|
215
|
+
Successful quarantine batches deliberately remain beside the former worktree; automatic GC is
|
|
216
|
+
out of scope because deleting a path still writable through an open descriptor would reintroduce
|
|
217
|
+
the data-loss race. A future cleanup flow must be explicit and inspectable.
|
|
125
218
|
- **Degrade, don't abort**: entry-level failures produce `warnings` — surfaced as a
|
|
126
219
|
server log line plus the project-scoped `rail.overlay_degraded` WS event — and the
|
|
127
220
|
spawn proceeds (a partial surface beats an aborted rail). `applyWorktreeOverlay` never
|
|
@@ -153,6 +246,24 @@ same repository lock as recovery, launch allocation and checkout, revalidates it
|
|
|
153
246
|
after waiting for that lock, and uses bounded Git/GitHub command timeouts so a wedged child cannot
|
|
154
247
|
block the repository forever.
|
|
155
248
|
|
|
249
|
+
Every path that observes an attached PR—continuation admission, Retry push, post-push
|
|
250
|
+
verification, Verify PR/poll, and Reopen—uses the same causal check: recorded PR identity, exact
|
|
251
|
+
head/base, lifecycle, and inclusion of immutable `delivery_sha`. Admission is read-only and accepts
|
|
252
|
+
only exact `OPEN` evidence; otherwise it refuses the launch without superseding the preserved card.
|
|
253
|
+
Immediately before any existing-PR push, including automatic second-iteration settlement, the local
|
|
254
|
+
`origin` must resolve to exactly one push URL identifying the repository that owns the same-repository
|
|
255
|
+
PR. A fork, multiple URLs, ambiguous URL, missing remote, or mismatch preserves the immutable SHA and
|
|
256
|
+
fails retryably without mutation. The push targets that already-verified raw URL, not the mutable
|
|
257
|
+
`origin` alias, closing the verification/use window. URLs with credential-bearing userinfo,
|
|
258
|
+
passwords, query strings, or fragments are rejected before they can enter process arguments or
|
|
259
|
+
persisted diagnostics; Git/`gh` credential helpers remain the authentication channel.
|
|
260
|
+
Exact `MERGED` evidence containing that SHA terminalizes immediately in a Retry/Reopen/Verify
|
|
261
|
+
decision action. A definitively stale/retargeted PR, or a `CLOSED`/`MERGED` PR without that SHA, is
|
|
262
|
+
detached by that action to `on_review` with the SHA and local branch preserved for a new PR.
|
|
263
|
+
Transient observation failure keeps the evidence and fails retryably.
|
|
264
|
+
Initial settlement records exact post-push `CLOSED` as `pr_closed`; exact post-push `MERGED` stays
|
|
265
|
+
attached and ready for Verify so that explicit decision can commit terminal ticket intent atomically.
|
|
266
|
+
|
|
156
267
|
- **create-pr** — deferred `deliverRailAsPr` (`server/rail-pr-delivery.ts`: 1 unit → its
|
|
157
268
|
branch; N → assembled onto the conventional batch branch off the integration branch, one PR
|
|
158
269
|
covering every ticket). The PR **title and canonical body are composed here** (see "Branch
|
|
@@ -167,24 +278,50 @@ block the repository forever.
|
|
|
167
278
|
labelled retryable. Tickets stay `on_review`.
|
|
168
279
|
- **publish** — `gh pr ready <url>` → `pr_ready` (the draft opens for the team's normal review).
|
|
169
280
|
Requires a real `pr_url`.
|
|
170
|
-
- **discard** — for a fresh delivery, best-effort `gh pr close <url
|
|
281
|
+
- **discard** — for a fresh delivery, best-effort `gh pr close <url>` without unleased remote-head
|
|
282
|
+
deletion; remove the launch's worktrees
|
|
171
283
|
(ledger rows closed as `failed`); delete only branches durably recorded as created by this
|
|
172
|
-
delivery
|
|
284
|
+
delivery only while each live local tip still equals its frozen SHA (owned per-unit sources plus
|
|
285
|
+
its exact assembled head; advanced or remote heads are preserved, preferred names are never
|
|
173
286
|
recomputed, and the integration branch is NEVER deleted); → `discarded`; revert
|
|
174
287
|
still-`on_review` tickets → `todo` (a manually re-triaged spec is respected) + Jira
|
|
175
288
|
`onRailDiscard`. Failures are retained in `cleanup_warnings` and disclosed. For a continuation,
|
|
176
289
|
discard can remove only the explicitly confirmed blocked local iteration: it preserves the
|
|
177
|
-
borrowed PR/head and review ticket state.
|
|
290
|
+
borrowed PR/head and review ticket state. Startup repairs migrated continuation ownership before
|
|
291
|
+
exposing actions, and per-unit `borrowed-pr` evidence independently keeps stale surfaces safe.
|
|
178
292
|
- **dismiss** — continuation acknowledgement. Clears the generation and owned clean
|
|
179
293
|
worktree while preserving the existing PR, head branch and `on_review` tickets.
|
|
180
294
|
- **acknowledge-no-changes** — fresh no-change acceptance → terminal `completed`, tickets Done,
|
|
181
295
|
with no PR/merge claim. Jira receives its own no-PR completion comment. Refine remains the
|
|
182
296
|
explicitly backlog-returning path and always uses Jira `todo`; it never inherits the configured
|
|
183
297
|
discard/cancellation status.
|
|
184
|
-
- **
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
298
|
+
- **recover-and-retry** — for one exact successful `settlement_interrupted` continuation, confirms
|
|
299
|
+
use of its recorded run/unit and authenticated isolated worktree. Authentication is repeated
|
|
300
|
+
immediately before staging; symlinks, the main checkout, reused directories and unregistered
|
|
301
|
+
paths fail closed. Private/overlay paths are excluded from add and from the final `commit --only`
|
|
302
|
+
pathspec; pre-staged forbidden entries are safely unstaged, hooks are bypassed, and the index is
|
|
303
|
+
re-audited. The candidate must fast-forward the live PR head. GitHub must prove a same-repository
|
|
304
|
+
PR and the local `origin` push URL must identify that exact PR repository before the immutable SHA
|
|
305
|
+
is pushed without force. A merge racing recovery returns verified success rather than a warning.
|
|
306
|
+
The user's main checkout is never switched, staged, stashed, committed, or cleaned by this action.
|
|
307
|
+
If the worktree was already removed, the same action scans refs, reflogs, and unreachable commits
|
|
308
|
+
before declaring the result absent. Baseline equality means “already delivered” only with an exact
|
|
309
|
+
durable final SHA/run marker, and means “no changes” only when every matching unit durably records
|
|
310
|
+
`changed=false` and `initialSha=finalSha=live PR head`. Otherwise the row becomes
|
|
311
|
+
`recovery_unavailable`: the card offers a secondary Recheck action, keeps Inspect/log access and
|
|
312
|
+
explains that Git evidence is local to the computer where the run executed instead of repeating
|
|
313
|
+
an impossible Commit or Checkout action.
|
|
314
|
+
- **poll-merge / Verify PR** — observes state, exact head/base/head OID, merge commit and PR commit
|
|
315
|
+
set. `OPEN` remains delivery-verified only while its exact head is `delivery_sha`; a missing
|
|
316
|
+
remote commit restores Retry push instead of merely saying “not merged”. `MERGED` becomes
|
|
317
|
+
`merged` immediately when immutable evidence contains `delivery_sha`, then tickets → `done` + Jira
|
|
318
|
+
`onRailMerged`. Exact `CLOSED` containing the SHA → `pr_closed`; missing-SHA terminal evidence
|
|
319
|
+
detaches to `on_review`. Successful retry/check responses carry the verified SHA, remote head and
|
|
320
|
+
whether a push actually ran for immediate user feedback.
|
|
321
|
+
- **reopen** — before and after `gh pr reopen`, revalidates exact PR/head/base and inclusion of
|
|
322
|
+
`delivery_sha`; mismatch or missing-SHA evidence detaches to `on_review` instead of retaining or
|
|
323
|
+
reopening the stale attachment. A valid reopen returns to draft or ready according to GitHub;
|
|
324
|
+
an exact merge racing the action terminalizes, while an exact still-closed result reports failure.
|
|
188
325
|
- **merge-local** — builds the complete merge in an isolated temporary checkout and advances the
|
|
189
326
|
user's revalidated clean base only after all branches succeed.
|
|
190
327
|
|
|
@@ -250,32 +387,86 @@ the `expectedDecision` they rendered.
|
|
|
250
387
|
one-shot `rail.pr_delivered`): broadcast at row INSERT (`building`), build-settle, and every
|
|
251
388
|
decision mutation. **`GET /rails`** returns `prDeliveries: Record<railIndex, snapshot>`
|
|
252
389
|
(newest non-terminal row per slot) for late-join hydration.
|
|
390
|
+
- **Monotonic generation arbitration.** Dashboard and Agent Chat retain terminal/superseded id
|
|
391
|
+
tombstones, accept a different id only through explicit lineage, strictly later `createdAt`, or
|
|
392
|
+
the exact `A.restoredFromDeliveryId=B` rollback after B terminalizes, and advance the same id only
|
|
393
|
+
through newer `updatedAt` evidence. A delayed A snapshot without that paired proof cannot replace
|
|
394
|
+
B, and a non-terminal replay cannot ordinarily reopen an id already observed terminal.
|
|
253
395
|
- **Option A — dashboard.** `client/src/context/RailPrDecisionContext.tsx` (registerHandler on
|
|
254
396
|
the shared socket + `GET /rails` seed) feeds `RailPrDecisionStrip` on `RailRow` (both density
|
|
255
397
|
branches): on_review → Create PR / Discard; pr_draft → Open PR + Publish / Discard (degraded →
|
|
256
|
-
retry); pr_ready →
|
|
257
|
-
retry-vs-blocked actions from `delivery_outcome`.
|
|
398
|
+
retry); pr_ready → Verify PR; no_changes → Done/refine; pr_closed → Reopen; pr_failed derives
|
|
399
|
+
retry-vs-blocked actions from `delivery_outcome`. A blocked legacy continuation without
|
|
400
|
+
`delivery_sha` never offers Checkout: it exposes any live preserved worktree through Inspect local
|
|
401
|
+
result and offers the confirmed Commit & retry push action above. A `recovery_unavailable` row
|
|
402
|
+
replaces that primary action with Check again; a clone without the original worktree/object never
|
|
403
|
+
claims that the implementation failed or that local progress was discarded. Ordinary Checkout is restricted
|
|
404
|
+
to a delivery with an immutable SHA; the server repeats that guard under the repo lock and refuses
|
|
405
|
+
a dirty or unreadable main checkout before releasing a worktree or changing any ref. Checkout
|
|
406
|
+
re-reads and uses only the current attached PR branch and immutable `delivery_sha` after acquiring
|
|
407
|
+
the repository lock, never the pre-lock snapshot. A same-named local/remote branch must equal that
|
|
408
|
+
SHA before switching, and the final branch plus HEAD are verified afterward; divergence is
|
|
409
|
+
preserved and a failed fast-forward can never be reported as success. Logs remain available after settle. Buttons
|
|
258
410
|
disable in flight and apply the authoritative HTTP snapshot immediately.
|
|
259
411
|
- **Option B — agent chat.** When the row carries `origin_conversation_id`, settle posts a
|
|
260
412
|
**persisted inline card**: a `'system'`-role `agent_messages` row (role is unconstrained TEXT —
|
|
261
413
|
no migration; TS unions widened) whose content is the `PrDecisionCardEnvelope` JSON.
|
|
262
|
-
`AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (update-in-place on
|
|
263
|
-
transition,
|
|
414
|
+
`AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (idempotent update-in-place on
|
|
415
|
+
every transition, transactionally consolidating legacy duplicate rows by delivery id) are reached via the
|
|
264
416
|
process-wide `server/agent-chat-registry.ts` singleton (null-safe: tests/disabled builds).
|
|
265
417
|
Live updates ride the app-global `agent_pr_decision` WS event; the client renders
|
|
266
|
-
`AgentPrDecisionCard` and POSTs the same project-scoped `/rails/pr-decision`.
|
|
267
|
-
|
|
418
|
+
`AgentPrDecisionCard` and POSTs the same project-scoped `/rails/pr-decision`. Client hydration,
|
|
419
|
+
WS upsert and rendering also dedupe by delivery id, so stale blocked and current ready cards can
|
|
420
|
+
never expose two contradictory Checkout/Discard action sets.
|
|
421
|
+
- **Race / hydration ABA guard:** a stale generation or occupied operation lease returns 409 before effects. Clients
|
|
268
422
|
distinguish `operation_in_progress` from a stale decision, show the active operation, disable
|
|
269
423
|
actions, apply the response snapshot, ignore terminal events for an older delivery id, and
|
|
270
|
-
hydrate again on focus/reconnect.
|
|
424
|
+
hydrate again on focus/reconnect. Each dashboard GET captures accepted per-rail mutation versions;
|
|
425
|
+
a live replacement or terminal deletion wins over its stale seed. Only accepted snapshots advance
|
|
426
|
+
that guard, so an ignored terminal for old A cannot make the same GET drop valid B.
|
|
427
|
+
Confirmation dialogs are bound to the exact delivery id/decision that opened them and disappear
|
|
428
|
+
when that generation changes; Checkout carries the same exact id through client and server, and
|
|
429
|
+
a rejected stale action never mutates or produces a success toast for B.
|
|
271
430
|
|
|
272
431
|
## Relaunch guard
|
|
273
432
|
|
|
274
433
|
`POST /rails/:i/launch` returns **409 `{ error: 'pr_decision_pending', prDeliveryId }`** for an
|
|
275
434
|
unresolved fresh delivery. An active `pr_draft`/`pr_ready` generation covering the same tickets is
|
|
276
|
-
a
|
|
277
|
-
|
|
278
|
-
|
|
435
|
+
a continuation contract only after exact `OPEN` identity, head/base, and `delivery_sha` evidence is
|
|
436
|
+
revalidated again under the repo lock, and its allocated worktree/local ref both equal that frozen
|
|
437
|
+
SHA. External PR discovery requires an explicit PR number or authoritative Jira key; the list match
|
|
438
|
+
is only discovery, and an exact authoritative PR view must freeze its live `headRefOid` before local
|
|
439
|
+
allocation. Local ticket numbers, title similarity, and unrelated `Fixes #<id>` text are not
|
|
440
|
+
authority. Any other evidence refuses admission and preserves
|
|
441
|
+
the existing card for Retry push or Verify PR; those decision actions terminalize exact `MERGED`
|
|
442
|
+
evidence or detach stale/missing-SHA terminal evidence to `on_review`. Only a verified continuation
|
|
443
|
+
is superseded atomically and run on that PR head. A stale
|
|
444
|
+
concurrent request receives 409 before it can allocate or claim anything. Legacy / non-isolated
|
|
445
|
+
launches are unaffected.
|
|
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.
|
|
279
470
|
|
|
280
471
|
## Ticket lifecycle — `on_review`
|
|
281
472
|
|
|
@@ -400,14 +591,25 @@ the launch INSERT. As-built:
|
|
|
400
591
|
- **D11 — premium decision UI**: disable-on-click reconciled to authoritative snapshots, tooltips
|
|
401
592
|
(base branch, PR url), designed degraded/failed states, distinct stale-vs-operation-busy 409 UX;
|
|
402
593
|
English source strings, all 8 locales, locale-parity green.
|
|
403
|
-
- **D12 — no automatic data loss**:
|
|
404
|
-
|
|
594
|
+
- **D12 — no automatic data loss**: live tracked/untracked/ignored state, HEAD, and branch ref are
|
|
595
|
+
revalidated before non-force automatic release; overlay roots are atomically moved into persistent
|
|
596
|
+
quarantine rather than unlinked; dirty, concurrent, unknown, user-ignored, ref-
|
|
597
|
+
mismatched, or advanced worktrees/branches are preserved.
|
|
405
598
|
- **D13 — one active generation**: migration 48 supersedes historical duplicates and a partial
|
|
406
|
-
unique index enforces one active row per rail
|
|
599
|
+
unique index enforces one active row per rail; snapshot consumers enforce lineage with
|
|
600
|
+
`createdAt`, same-id order with `updatedAt`, terminal/superseded tombstones, exact-generation
|
|
601
|
+
confirmations, an accepted-mutation ABA guard, and one exact failed-replacement rollback proof.
|
|
407
602
|
- **D14 — operation lease before effects**: a decision claims before Git/GitHub/ticket work;
|
|
408
603
|
the loser has no external side effects and HTTP returns the authoritative snapshot.
|
|
409
|
-
- **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`;
|
|
410
|
-
|
|
604
|
+
- **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`; admission,
|
|
605
|
+
retry/post-push, poll and reopen all validate exact PR/head/base plus SHA inclusion. Admission is
|
|
606
|
+
read-only and refuses non-exact continuations; inferred external PRs freeze the authoritative live
|
|
607
|
+
head before worktree allocation, post-run verification permits only a matched HEAD/ref advance,
|
|
608
|
+
and pre-push settlement freezes the new exact SHA and proves `origin` owns the attached PR before
|
|
609
|
+
every automatic or user-triggered push. Decision actions terminalize exact MERGED evidence
|
|
610
|
+
or detach stale/retargeted/terminal-without-SHA attachments to `on_review` with the SHA preserved.
|
|
611
|
+
Legacy recovery accepts only a unique run marker in the commit
|
|
612
|
+
subject. Ambiguous PR creation resolves exact open head/base identity.
|
|
411
613
|
- **D16 — crash recovery before admission**: startup preserves successful interrupted work and
|
|
412
614
|
makes every stale `building` generation actionable before another launch can reuse its path.
|
|
413
615
|
- **D17 — ownership-aware continuation cleanup**: dismiss/discard never closes or deletes the
|
|
@@ -417,6 +619,16 @@ the launch INSERT. As-built:
|
|
|
417
619
|
mutating newer work, and terminal Agent cards are re-projected.
|
|
418
620
|
- **D19 — truthful no-change completion**: fresh no-change has Mark done (`completed`) and Refine;
|
|
419
621
|
a continuation only dismisses its borrowed follow-up.
|
|
622
|
+
- **D20 — preserved-result recovery**: startup also recovers one uniquely run-marked unreachable
|
|
623
|
+
commit; otherwise both cards hide misleading Checkout, reveal live delivery-owned worktrees, and
|
|
624
|
+
offer a lease-guarded Commit & retry push that is exact-baseline, fast-forward, immutable-SHA and
|
|
625
|
+
non-force while leaving the user's main checkout untouched. Recovery worktrees, commit index
|
|
626
|
+
pathspecs, same-repository PR ownership, origin identity, and checkout cleanliness all fail closed.
|
|
627
|
+
- **D21 — orphan protection and truthful device-local fallback**: startup and manual Recheck share
|
|
628
|
+
one bounded refs/reflogs/unreachable scan; an exact orphan is pinned under an internal recovery ref
|
|
629
|
+
before durable/network mutation. No evidence, ambiguous evidence, or a later unrelated branch tip
|
|
630
|
+
stays non-destructive and renders localized other-computer guidance instead of looping Commit or
|
|
631
|
+
Checkout.
|
|
420
632
|
|
|
421
633
|
## Kill-switch
|
|
422
634
|
|