specrails-desktop 2.29.2 → 2.29.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/client/dist/assets/{ActivityFeedPage-ClpD5CFj.js → ActivityFeedPage-CFB4UBhT.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-C-T0r16B.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-IljV5FOQ.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-B5miH10_.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-BgYQrAcH.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BtVCuNjL.js} +1 -1
  7. package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Cb_jAjSV.js} +1 -1
  8. package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-s_Cc_JL1.js} +1 -1
  9. package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-Cr47QI9L.js} +1 -1
  10. package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-C_WPRuG_.js} +1 -1
  11. package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-NfFOciRB.js} +1 -1
  12. package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DqEW7Z7K.js} +1 -1
  13. package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-D4ppEpDE.js} +1 -1
  14. package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-DStSoMyq.js} +1 -1
  15. package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-DltCwS4P.js} +1 -1
  16. package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-BWaiAbUq.js} +1 -1
  17. package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-BqjbXuBZ.js} +1 -1
  18. package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext--o3JAM9F.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-B-UOmzXw.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-DkGajIxE.js} +1 -1
  21. package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-5CUXlkw7.js} +1 -1
  22. package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-B_igqmti.js} +4 -4
  23. package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-DDMIBBUd.js} +1 -1
  24. package/client/dist/assets/agent-BXc59ARK2.js +1 -0
  25. package/client/dist/assets/agent-BbIFXdDq.js +1 -0
  26. package/client/dist/assets/agent-BqEfA5u5.js +1 -0
  27. package/client/dist/assets/agent-BtGzgTC4.js +1 -0
  28. package/client/dist/assets/agent-CIhys_5d.js +1 -0
  29. package/client/dist/assets/agent-CJPaqsVR.js +1 -0
  30. package/client/dist/assets/agent-n-hY4GuL.js +1 -0
  31. package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
  32. package/client/dist/assets/common-6MLbD-jM.js +1 -0
  33. package/client/dist/assets/common-BKGEu4rm.js +1 -0
  34. package/client/dist/assets/common-BNbnv8cE2.js +1 -0
  35. package/client/dist/assets/common-BqGWtDbt.js +1 -0
  36. package/client/dist/assets/common-DBtTzeYm2.js +1 -0
  37. package/client/dist/assets/common-DEW3Nr8n.js +1 -0
  38. package/client/dist/assets/{common-DVQuhs-m.js → common-DHsv9N5d.js} +1 -1
  39. package/client/dist/assets/common-mkrEcW-Y.js +1 -0
  40. package/client/dist/assets/dashboard-BYzxom0L.js +1 -0
  41. package/client/dist/assets/dashboard-BmDZmHyY.js +1 -0
  42. package/client/dist/assets/dashboard-CnNtJBQ5.js +1 -0
  43. package/client/dist/assets/dashboard-D4Uj-XWc2.js +1 -0
  44. package/client/dist/assets/dashboard-D57uRaGw2.js +1 -0
  45. package/client/dist/assets/dashboard-DAr6F9et.js +1 -0
  46. package/client/dist/assets/dashboard-vAcsn0L3.js +1 -0
  47. package/client/dist/assets/dashboard-vrjZvxWy.js +1 -0
  48. package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-BCUhGUXS.js} +1 -1
  49. package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-DcoXTnEI.js} +1 -1
  50. package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-DP3Ab6fX.js} +1 -1
  51. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DdKKL8Fx.js} +1 -1
  52. package/client/dist/assets/{i18n-CgaRxpII.js → i18n-DREQCE2F.js} +2 -2
  53. package/client/dist/assets/index-B784m8RT.js +76 -0
  54. package/client/dist/assets/index-CQBIemiz.css +2 -0
  55. package/client/dist/assets/{spending-C4p06nen.js → spending-CtY7yYes.js} +1 -1
  56. package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-BFBdW59q.js} +1 -1
  57. package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-uWVZ8eia.js} +1 -1
  58. package/client/dist/index.html +14 -14
  59. package/docs/internals/safe-pr-review-flow.md +221 -38
  60. package/package.json +1 -1
  61. package/server/dist/active-pr-continuation.js +237 -52
  62. package/server/dist/agent-chat-manager.js +29 -12
  63. package/server/dist/agent-store.js +19 -4
  64. package/server/dist/db.js +21 -0
  65. package/server/dist/pr-lifecycle.js +100 -8
  66. package/server/dist/project-git.js +62 -2
  67. package/server/dist/rail-isolated-launch.js +644 -108
  68. package/server/dist/rail-pr-decision.js +1065 -181
  69. package/server/dist/rail-pr-recovery-git.js +235 -0
  70. package/server/dist/rail-pr-store.js +120 -9
  71. package/server/dist/rail-worktree-release.js +290 -4
  72. package/server/dist/rails-router.js +97 -9
  73. package/server/dist/worktree-manager.js +120 -16
  74. package/server/dist/worktree-overlay.js +166 -4
  75. package/client/dist/assets/agent-4rmXoo_p.js +0 -1
  76. package/client/dist/assets/agent-BxZuNGih.js +0 -1
  77. package/client/dist/assets/agent-C3uXOYv_.js +0 -1
  78. package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
  79. package/client/dist/assets/agent-CASKrhG12.js +0 -1
  80. package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
  81. package/client/dist/assets/agent-DitUxYh2.js +0 -1
  82. package/client/dist/assets/agent-Doow4X7o.js +0 -1
  83. package/client/dist/assets/common-CD1QHriA2.js +0 -1
  84. package/client/dist/assets/common-CwwPaRxx.js +0 -1
  85. package/client/dist/assets/common-Dg2jaX7d.js +0 -1
  86. package/client/dist/assets/common-DuBwj6tK.js +0 -1
  87. package/client/dist/assets/common-MRj9MHcc.js +0 -1
  88. package/client/dist/assets/common-O4ZpnMLp.js +0 -1
  89. package/client/dist/assets/common-fyeF0v7C2.js +0 -1
  90. package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
  91. package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
  92. package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
  93. package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
  94. package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
  95. package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
  96. package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
  97. package/client/dist/assets/dashboard-owcDucDi.js +0 -1
  98. package/client/dist/assets/index-BflYQpWm.css +0 -2
  99. package/client/dist/assets/index-C1_-BTXy.js +0 -72
@@ -1 +1 @@
1
- import{o as e}from"./i18n-CgaRxpII.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
+ import{o as e}from"./i18n-DREQCE2F.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-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 +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-CgaRxpII.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};
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-DREQCE2F.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};
@@ -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-C1_-BTXy.js"></script>
207
+ <script type="module" crossorigin src="/assets/index-B784m8RT.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-4rmXoo_p.js">
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-DVQuhs-m.js">
227
- <link rel="modulepreload" crossorigin href="/assets/dashboard-CpCmJvMB.js">
226
+ <link rel="modulepreload" crossorigin href="/assets/common-DHsv9N5d.js">
227
+ <link rel="modulepreload" crossorigin href="/assets/dashboard-BmDZmHyY.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-CgaRxpII.js">
241
- <link rel="modulepreload" crossorigin href="/assets/Trans-B_vKZ-Hb.js">
240
+ <link rel="modulepreload" crossorigin href="/assets/i18n-DREQCE2F.js">
241
+ <link rel="modulepreload" crossorigin href="/assets/Trans-DDMIBBUd.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-2OqX0vap.js">
246
+ <link rel="modulepreload" crossorigin href="/assets/useBrowserCaptureSession-BFBdW59q.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-X8YB9xzv.js">
252
- <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-DiFvZDPW.js">
251
+ <link rel="modulepreload" crossorigin href="/assets/useDesktop-uWVZ8eia.js">
252
+ <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext--o3JAM9F.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-BxHAksaU.js">
276
- <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-ByHIauTQ.js">
277
- <link rel="modulepreload" crossorigin href="/assets/spending-C4p06nen.js">
278
- <link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-9UEyK974.js">
279
- <link rel="stylesheet" crossorigin href="/assets/index-BflYQpWm.css">
275
+ <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-DdKKL8Fx.js">
276
+ <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-DP3Ab6fX.js">
277
+ <link rel="modulepreload" crossorigin href="/assets/spending-CtY7yYes.js">
278
+ <link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-B_igqmti.js">
279
+ <link rel="stylesheet" crossorigin href="/assets/index-CQBIemiz.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–50; CRUD in
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 can never reappear after
16
- B closes. `supersedes_delivery_id` preserves that lineage and `is_continuation` records that the
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
- GitHub reports CLOSED, not merged → pr_closed
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 but never
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 destructive “Discard local result” confirmation, while still
60
- preserving the external PR/branch. Fresh deliveries keep the full discard semantics.
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,64 @@ 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. Dirty, unknown, or ref-mismatched worktrees become
83
- `needs-review` and are never force-removed automatically. Push failure retains the exact SHA.
121
+ - Clean, committed worktrees may be released only after a final live preflight proves tracked,
122
+ untracked, and ignored status (apart from durably recorded Specrails overlay paths), plus exact
123
+ worktree HEAD and branch ref. Authenticated overlay roots are atomically renamed to a unique
124
+ persistent same-filesystem quarantine batch and revalidated there; automatic cleanup never unlinks that
125
+ quarantine, so a raced replacement or open-descriptor write remains recoverable. The batch root is
126
+ persisted before the first move and pointers are never evicted while their bytes remain on disk. A recreated
127
+ original path makes final non-force removal fail closed. Each quarantine batch root is persisted and shown
128
+ in a collapsed card section without changing an otherwise successful delivery to
129
+ `cleanup_incomplete`. If a terminal dashboard action creates an archive, a persistent notification
130
+ exposes its exact paths and a Copy paths action before the card is removed. Dirty, unknown,
131
+ ignored-user-data, concurrent-write, or ref-
132
+ mismatched worktrees become retryable `needs-review`; push failure retains the exact SHA.
84
133
  - Project admission remains closed while startup reconciliation runs under the repo lock. Every
85
134
  stale `building` shape becomes actionable; successful interrupted work is preserved and labelled
86
135
  `settlement_interrupted`, while only all-failed runs become `implementation_failed`.
87
136
  - Operation leases are process capabilities. Startup clears every prior-process lease before card
88
137
  projection, marks the still-active delivery `operation_interrupted`, and leaves its durable SHA,
89
138
  PR and unit evidence intact so the user can safely retry.
139
+ - Migrated successful `settlement_interrupted` and `recovery_unavailable` rows with an existing PR
140
+ are re-examined at startup. Startup and the explicit recovery action share one bounded,
141
+ fail-closed scan over refs, reflogs, and unreachable objects; a command/read failure, malformed
142
+ output, excessive combined candidate set, or ambiguous marker can never become false proof of a
143
+ unique result. Every refs/reflogs, unreachable-object, or recorded-branch candidate must carry the unique `(run <id>)` settlement
144
+ marker in its **commit subject**; branch/worktree association or a marker found only in the body
145
+ is never causal proof. The unreachable-object scan runs at most once per serialized reconciliation,
146
+ accepts only a bounded, unambiguous set of commit objects, and fails closed on malformed or excessive
147
+ output. A terminal `released`/legacy `failed` branch tip is only a fallback under
148
+ that same subject rule. One unambiguous run-owned commit becomes `delivery_sha` and a Retry push action
149
+ when absent from the recorded open PR. If GitHub already exposes that exact causal commit, the
150
+ row is classified as delivered without a no-op push. Startup audits earlier legacy recoveries
151
+ that already reached draft/ready too, replacing a frozen old PR-head SHA with the run-owned
152
+ commit when needed. Existing-PR recovery repairs `is_continuation=true` before exposing actions;
153
+ per-unit `branchOwnership=borrowed-pr|preexisting` independently prevents branch deletion without
154
+ incorrectly making every fresh replacement PR a borrowed continuation.
155
+ Before persisting or pushing a recovered orphan, Specrails pins it under a delivery-specific
156
+ `refs/specrails/recovery/*` ref. The ref survives crashes and protects the object from Git garbage
157
+ collection until exact remote delivery is proved or the user explicitly discards the result.
158
+ Discovery still runs when a ledger path exists but is dirty, recreated, or unauthenticated; if a
159
+ distinct worktree result and run-owned orphan coexist, both are preserved and neither is selected,
160
+ bundled, or pushed automatically. Restart preserves a `recover-and-retry` recovery-family status,
161
+ and explicit discard may remove a pre-persistence ref only after its exact run/final ownership is
162
+ re-proven. If the ref was advanced from marker-bearing ancestor A to descendant B before a crash,
163
+ that exact delivery ref selects B rather than letting the global scan manufacture self-ambiguity.
164
+ Successful successor delivery and startup settlement retry clean up exact ancestor refs.
165
+ A later unrelated branch tip is never substituted for the durable final SHA or unique run-marked
166
+ object, and all matching units are updated together. Dirty, missing, mismatched, unmarked or
167
+ ambiguous evidence stays blocked and preserved. Once the exact causal SHA is proven, a transient
168
+ GitHub lookup instead stays retryable and Retry push revalidates before mutation.
169
+ - A blocked legacy card exposes a worktree path only after the path is proved absolute,
170
+ non-symlink, outside the main checkout, uniquely present in this repository's live worktree
171
+ registry, and checked out on the recorded branch/HEAD. Historical or other-device paths are
172
+ removed from the snapshot instead of being presented as available.
90
173
  - `GET /rails` is hydration-only and never invokes crash recovery. This avoids misclassifying the
91
174
  normal live window between durable loop completion and commit/ref/push settlement.
92
175
  - Startup retries pending terminal ticket effects in-process and re-projects active and terminal
@@ -116,12 +199,17 @@ entries. Each allocated worktree therefore gets `applyWorktreeOverlay(...)` at l
116
199
  `worktrees/` entry are never linked (nested pipeline worktrees stay local). `.mcp.json`
117
200
  and the instruction file (CLAUDE.md/AGENTS.md/GEMINI.md per provider) are COPIED when
118
201
  the checkout lacks them. Windows: junction → dereferencing-copy fallback.
119
- - **Never on the PR**: every overlay-owned path lands in a worktree-local manifest
120
- (`.sr-rail-overlay.json`, unioned across passes so a RESUMED worktree keeps prior
121
- entries claimed) and is excluded from `commitWorktree`'s stage via `:(exclude)`
122
- pathspecs. Excluded paths are untracked by construction, so no real work can be lost;
123
- a no-op overlay keeps the byte-identical `git add -A`. Worktree removal (`git worktree
124
- remove --force`) deletes the symlinks as entries without following them.
202
+ - **Never on the PR**: every allocator-created overlay path is excluded from staging with a literal
203
+ top-level pathspec. The writable worktree manifest is not cleanup authority; settlement persists
204
+ fingerprints captured from the trusted source/created entries. Cleanup atomically renames each
205
+ authenticated root into a unique sibling quarantine, revalidates it after the move, discloses
206
+ raced content there, and never deletes or restores quarantined paths automatically over a
207
+ possibly recreated source. A modified or
208
+ concurrently extended overlay is preserved as user data, while a no-op
209
+ overlay keeps the byte-identical ordinary staging behavior.
210
+ Successful quarantine batches deliberately remain beside the former worktree; automatic GC is
211
+ out of scope because deleting a path still writable through an open descriptor would reintroduce
212
+ the data-loss race. A future cleanup flow must be explicit and inspectable.
125
213
  - **Degrade, don't abort**: entry-level failures produce `warnings` — surfaced as a
126
214
  server log line plus the project-scoped `rail.overlay_degraded` WS event — and the
127
215
  spawn proceeds (a partial surface beats an aborted rail). `applyWorktreeOverlay` never
@@ -153,6 +241,24 @@ same repository lock as recovery, launch allocation and checkout, revalidates it
153
241
  after waiting for that lock, and uses bounded Git/GitHub command timeouts so a wedged child cannot
154
242
  block the repository forever.
155
243
 
244
+ Every path that observes an attached PR—continuation admission, Retry push, post-push
245
+ verification, Verify PR/poll, and Reopen—uses the same causal check: recorded PR identity, exact
246
+ head/base, lifecycle, and inclusion of immutable `delivery_sha`. Admission is read-only and accepts
247
+ only exact `OPEN` evidence; otherwise it refuses the launch without superseding the preserved card.
248
+ Immediately before any existing-PR push, including automatic second-iteration settlement, the local
249
+ `origin` must resolve to exactly one push URL identifying the repository that owns the same-repository
250
+ PR. A fork, multiple URLs, ambiguous URL, missing remote, or mismatch preserves the immutable SHA and
251
+ fails retryably without mutation. The push targets that already-verified raw URL, not the mutable
252
+ `origin` alias, closing the verification/use window. URLs with credential-bearing userinfo,
253
+ passwords, query strings, or fragments are rejected before they can enter process arguments or
254
+ persisted diagnostics; Git/`gh` credential helpers remain the authentication channel.
255
+ Exact `MERGED` evidence containing that SHA terminalizes immediately in a Retry/Reopen/Verify
256
+ decision action. A definitively stale/retargeted PR, or a `CLOSED`/`MERGED` PR without that SHA, is
257
+ detached by that action to `on_review` with the SHA and local branch preserved for a new PR.
258
+ Transient observation failure keeps the evidence and fails retryably.
259
+ Initial settlement records exact post-push `CLOSED` as `pr_closed`; exact post-push `MERGED` stays
260
+ attached and ready for Verify so that explicit decision can commit terminal ticket intent atomically.
261
+
156
262
  - **create-pr** — deferred `deliverRailAsPr` (`server/rail-pr-delivery.ts`: 1 unit → its
157
263
  branch; N → assembled onto the conventional batch branch off the integration branch, one PR
158
264
  covering every ticket). The PR **title and canonical body are composed here** (see "Branch
@@ -167,24 +273,50 @@ block the repository forever.
167
273
  labelled retryable. Tickets stay `on_review`.
168
274
  - **publish** — `gh pr ready <url>` → `pr_ready` (the draft opens for the team's normal review).
169
275
  Requires a real `pr_url`.
170
- - **discard** — for a fresh delivery, best-effort `gh pr close <url> --delete-branch`; remove the launch's worktrees
276
+ - **discard** — for a fresh delivery, best-effort `gh pr close <url>` without unleased remote-head
277
+ deletion; remove the launch's worktrees
171
278
  (ledger rows closed as `failed`); delete only branches durably recorded as created by this
172
- delivery (owned per-unit sources plus its exact assembled head; preferred names are never
279
+ delivery only while each live local tip still equals its frozen SHA (owned per-unit sources plus
280
+ its exact assembled head; advanced or remote heads are preserved, preferred names are never
173
281
  recomputed, and the integration branch is NEVER deleted); → `discarded`; revert
174
282
  still-`on_review` tickets → `todo` (a manually re-triaged spec is respected) + Jira
175
283
  `onRailDiscard`. Failures are retained in `cleanup_warnings` and disclosed. For a continuation,
176
284
  discard can remove only the explicitly confirmed blocked local iteration: it preserves the
177
- borrowed PR/head and review ticket state.
285
+ borrowed PR/head and review ticket state. Startup repairs migrated continuation ownership before
286
+ exposing actions, and per-unit `borrowed-pr` evidence independently keeps stale surfaces safe.
178
287
  - **dismiss** — continuation acknowledgement. Clears the generation and owned clean
179
288
  worktree while preserving the existing PR, head branch and `on_review` tickets.
180
289
  - **acknowledge-no-changes** — fresh no-change acceptance → terminal `completed`, tickets Done,
181
290
  with no PR/merge claim. Jira receives its own no-PR completion comment. Refine remains the
182
291
  explicitly backlog-returning path and always uses Jira `todo`; it never inherits the configured
183
292
  discard/cancellation status.
184
- - **poll-merge** — observes state, exact head/base/head OID, merge commit and PR commit set;
185
- `MERGED` becomes `merged` only when that immutable evidence contains `delivery_sha`, then tickets
186
- → `done` + Jira `onRailMerged`. `OPEN` is observation-only; `CLOSED` without merge → `pr_closed`.
187
- - **reopen** — `gh pr reopen` from `pr_closed`, returning to draft or ready according to GitHub.
293
+ - **recover-and-retry** — for one exact successful `settlement_interrupted` continuation, confirms
294
+ use of its recorded run/unit and authenticated isolated worktree. Authentication is repeated
295
+ immediately before staging; symlinks, the main checkout, reused directories and unregistered
296
+ paths fail closed. Private/overlay paths are excluded from add and from the final `commit --only`
297
+ pathspec; pre-staged forbidden entries are safely unstaged, hooks are bypassed, and the index is
298
+ re-audited. The candidate must fast-forward the live PR head. GitHub must prove a same-repository
299
+ PR and the local `origin` push URL must identify that exact PR repository before the immutable SHA
300
+ is pushed without force. A merge racing recovery returns verified success rather than a warning.
301
+ The user's main checkout is never switched, staged, stashed, committed, or cleaned by this action.
302
+ If the worktree was already removed, the same action scans refs, reflogs, and unreachable commits
303
+ before declaring the result absent. Baseline equality means “already delivered” only with an exact
304
+ durable final SHA/run marker, and means “no changes” only when every matching unit durably records
305
+ `changed=false` and `initialSha=finalSha=live PR head`. Otherwise the row becomes
306
+ `recovery_unavailable`: the card offers a secondary Recheck action, keeps Inspect/log access and
307
+ explains that Git evidence is local to the computer where the run executed instead of repeating
308
+ an impossible Commit or Checkout action.
309
+ - **poll-merge / Verify PR** — observes state, exact head/base/head OID, merge commit and PR commit
310
+ set. `OPEN` remains delivery-verified only while its exact head is `delivery_sha`; a missing
311
+ remote commit restores Retry push instead of merely saying “not merged”. `MERGED` becomes
312
+ `merged` immediately when immutable evidence contains `delivery_sha`, then tickets → `done` + Jira
313
+ `onRailMerged`. Exact `CLOSED` containing the SHA → `pr_closed`; missing-SHA terminal evidence
314
+ detaches to `on_review`. Successful retry/check responses carry the verified SHA, remote head and
315
+ whether a push actually ran for immediate user feedback.
316
+ - **reopen** — before and after `gh pr reopen`, revalidates exact PR/head/base and inclusion of
317
+ `delivery_sha`; mismatch or missing-SHA evidence detaches to `on_review` instead of retaining or
318
+ reopening the stale attachment. A valid reopen returns to draft or ready according to GitHub;
319
+ an exact merge racing the action terminalizes, while an exact still-closed result reports failure.
188
320
  - **merge-local** — builds the complete merge in an isolated temporary checkout and advances the
189
321
  user's revalidated clean base only after all branches succeed.
190
322
 
@@ -250,32 +382,62 @@ the `expectedDecision` they rendered.
250
382
  one-shot `rail.pr_delivered`): broadcast at row INSERT (`building`), build-settle, and every
251
383
  decision mutation. **`GET /rails`** returns `prDeliveries: Record<railIndex, snapshot>`
252
384
  (newest non-terminal row per slot) for late-join hydration.
385
+ - **Monotonic generation arbitration.** Dashboard and Agent Chat retain terminal/superseded id
386
+ tombstones, accept a different id only through explicit lineage, strictly later `createdAt`, or
387
+ the exact `A.restoredFromDeliveryId=B` rollback after B terminalizes, and advance the same id only
388
+ through newer `updatedAt` evidence. A delayed A snapshot without that paired proof cannot replace
389
+ B, and a non-terminal replay cannot ordinarily reopen an id already observed terminal.
253
390
  - **Option A — dashboard.** `client/src/context/RailPrDecisionContext.tsx` (registerHandler on
254
391
  the shared socket + `GET /rails` seed) feeds `RailPrDecisionStrip` on `RailRow` (both density
255
392
  branches): on_review → Create PR / Discard; pr_draft → Open PR + Publish / Discard (degraded →
256
- retry); pr_ready → Check merge; no_changes → Done/refine; pr_closed → Reopen; pr_failed derives
257
- retry-vs-blocked actions from `delivery_outcome`. Logs remain available after settle. Buttons
393
+ retry); pr_ready → Verify PR; no_changes → Done/refine; pr_closed → Reopen; pr_failed derives
394
+ retry-vs-blocked actions from `delivery_outcome`. A blocked legacy continuation without
395
+ `delivery_sha` never offers Checkout: it exposes any live preserved worktree through Inspect local
396
+ result and offers the confirmed Commit & retry push action above. A `recovery_unavailable` row
397
+ replaces that primary action with Check again; a clone without the original worktree/object never
398
+ claims that the implementation failed or that local progress was discarded. Ordinary Checkout is restricted
399
+ to a delivery with an immutable SHA; the server repeats that guard under the repo lock and refuses
400
+ a dirty or unreadable main checkout before releasing a worktree or changing any ref. Checkout
401
+ re-reads and uses only the current attached PR branch and immutable `delivery_sha` after acquiring
402
+ the repository lock, never the pre-lock snapshot. A same-named local/remote branch must equal that
403
+ SHA before switching, and the final branch plus HEAD are verified afterward; divergence is
404
+ preserved and a failed fast-forward can never be reported as success. Logs remain available after settle. Buttons
258
405
  disable in flight and apply the authoritative HTTP snapshot immediately.
259
406
  - **Option B — agent chat.** When the row carries `origin_conversation_id`, settle posts a
260
407
  **persisted inline card**: a `'system'`-role `agent_messages` row (role is unconstrained TEXT —
261
408
  no migration; TS unions widened) whose content is the `PrDecisionCardEnvelope` JSON.
262
- `AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (update-in-place on every
263
- transition, so cold-load shows terminal states correctly) are reached from rails code via the
409
+ `AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (idempotent update-in-place on
410
+ every transition, transactionally consolidating legacy duplicate rows by delivery id) are reached via the
264
411
  process-wide `server/agent-chat-registry.ts` singleton (null-safe: tests/disabled builds).
265
412
  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
- - **Race:** a stale generation or occupied operation lease returns 409 before effects. Clients
413
+ `AgentPrDecisionCard` and POSTs the same project-scoped `/rails/pr-decision`. Client hydration,
414
+ WS upsert and rendering also dedupe by delivery id, so stale blocked and current ready cards can
415
+ never expose two contradictory Checkout/Discard action sets.
416
+ - **Race / hydration ABA guard:** a stale generation or occupied operation lease returns 409 before effects. Clients
268
417
  distinguish `operation_in_progress` from a stale decision, show the active operation, disable
269
418
  actions, apply the response snapshot, ignore terminal events for an older delivery id, and
270
- hydrate again on focus/reconnect.
419
+ hydrate again on focus/reconnect. Each dashboard GET captures accepted per-rail mutation versions;
420
+ a live replacement or terminal deletion wins over its stale seed. Only accepted snapshots advance
421
+ that guard, so an ignored terminal for old A cannot make the same GET drop valid B.
422
+ Confirmation dialogs are bound to the exact delivery id/decision that opened them and disappear
423
+ when that generation changes; Checkout carries the same exact id through client and server, and
424
+ a rejected stale action never mutates or produces a success toast for B.
271
425
 
272
426
  ## Relaunch guard
273
427
 
274
428
  `POST /rails/:i/launch` returns **409 `{ error: 'pr_decision_pending', prDeliveryId }`** for an
275
429
  unresolved fresh delivery. An active `pr_draft`/`pr_ready` generation covering the same tickets is
276
- a verified continuation contract instead: launch revalidates it again under the repo lock,
277
- supersedes it atomically and runs only on that PR head. A stale concurrent request receives 409
278
- before it can allocate or claim anything. Legacy / non-isolated launches are unaffected.
430
+ a continuation contract only after exact `OPEN` identity, head/base, and `delivery_sha` evidence is
431
+ revalidated again under the repo lock, and its allocated worktree/local ref both equal that frozen
432
+ SHA. External PR discovery requires an explicit PR number or authoritative Jira key; the list match
433
+ is only discovery, and an exact authoritative PR view must freeze its live `headRefOid` before local
434
+ allocation. Local ticket numbers, title similarity, and unrelated `Fixes #<id>` text are not
435
+ authority. Any other evidence refuses admission and preserves
436
+ the existing card for Retry push or Verify PR; those decision actions terminalize exact `MERGED`
437
+ evidence or detach stale/missing-SHA terminal evidence to `on_review`. Only a verified continuation
438
+ is superseded atomically and run on that PR head. A stale
439
+ concurrent request receives 409 before it can allocate or claim anything. Legacy / non-isolated
440
+ launches are unaffected.
279
441
 
280
442
  ## Ticket lifecycle — `on_review`
281
443
 
@@ -400,14 +562,25 @@ the launch INSERT. As-built:
400
562
  - **D11 — premium decision UI**: disable-on-click reconciled to authoritative snapshots, tooltips
401
563
  (base branch, PR url), designed degraded/failed states, distinct stale-vs-operation-busy 409 UX;
402
564
  English source strings, all 8 locales, locale-parity green.
403
- - **D12 — no automatic data loss**: dirty, unknown and ref-mismatched worktrees are
404
- `needs-review`; only explicit consequence-specific cleanup may force-remove them.
565
+ - **D12 — no automatic data loss**: live tracked/untracked/ignored state, HEAD, and branch ref are
566
+ revalidated before non-force automatic release; overlay roots are atomically moved into persistent
567
+ quarantine rather than unlinked; dirty, concurrent, unknown, user-ignored, ref-
568
+ mismatched, or advanced worktrees/branches are preserved.
405
569
  - **D13 — one active generation**: migration 48 supersedes historical duplicates and a partial
406
- unique index enforces one active row per rail.
570
+ unique index enforces one active row per rail; snapshot consumers enforce lineage with
571
+ `createdAt`, same-id order with `updatedAt`, terminal/superseded tombstones, exact-generation
572
+ confirmations, an accepted-mutation ABA guard, and one exact failed-replacement rollback proof.
407
573
  - **D14 — operation lease before effects**: a decision claims before Git/GitHub/ticket work;
408
574
  the loser has no external side effects and HTTP returns the authoritative snapshot.
409
- - **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`; ambiguous PR
410
- creation resolves exact open head/base identity; CLOSED is distinct and reopenable.
575
+ - **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`; admission,
576
+ retry/post-push, poll and reopen all validate exact PR/head/base plus SHA inclusion. Admission is
577
+ read-only and refuses non-exact continuations; inferred external PRs freeze the authoritative live
578
+ head before worktree allocation, post-run verification permits only a matched HEAD/ref advance,
579
+ and pre-push settlement freezes the new exact SHA and proves `origin` owns the attached PR before
580
+ every automatic or user-triggered push. Decision actions terminalize exact MERGED evidence
581
+ or detach stale/retargeted/terminal-without-SHA attachments to `on_review` with the SHA preserved.
582
+ Legacy recovery accepts only a unique run marker in the commit
583
+ subject. Ambiguous PR creation resolves exact open head/base identity.
411
584
  - **D16 — crash recovery before admission**: startup preserves successful interrupted work and
412
585
  makes every stale `building` generation actionable before another launch can reuse its path.
413
586
  - **D17 — ownership-aware continuation cleanup**: dismiss/discard never closes or deletes the
@@ -417,6 +590,16 @@ the launch INSERT. As-built:
417
590
  mutating newer work, and terminal Agent cards are re-projected.
418
591
  - **D19 — truthful no-change completion**: fresh no-change has Mark done (`completed`) and Refine;
419
592
  a continuation only dismisses its borrowed follow-up.
593
+ - **D20 — preserved-result recovery**: startup also recovers one uniquely run-marked unreachable
594
+ commit; otherwise both cards hide misleading Checkout, reveal live delivery-owned worktrees, and
595
+ offer a lease-guarded Commit & retry push that is exact-baseline, fast-forward, immutable-SHA and
596
+ non-force while leaving the user's main checkout untouched. Recovery worktrees, commit index
597
+ pathspecs, same-repository PR ownership, origin identity, and checkout cleanliness all fail closed.
598
+ - **D21 — orphan protection and truthful device-local fallback**: startup and manual Recheck share
599
+ one bounded refs/reflogs/unreachable scan; an exact orphan is pinned under an internal recovery ref
600
+ before durable/network mutation. No evidence, ambiguous evidence, or a later unrelated branch tip
601
+ stays non-destructive and renders localized other-computer guidance instead of looping Commit or
602
+ Checkout.
420
603
 
421
604
  ## Kill-switch
422
605
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.29.2",
3
+ "version": "2.29.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",