specrails-desktop 2.29.3 → 2.29.4

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