@@ -56,6 +56,7 @@ interface TestFixture {
5656
5757let fixture : TestFixture ;
5858const wsRequests : WsRequestEnvelope [ "body" ] [ ] = [ ] ;
59+ let customWsRpcResolver : ( ( body : WsRequestEnvelope [ "body" ] ) => unknown | undefined ) | null = null ;
5960const wsLink = ws . link ( / w s ( s ) ? : \/ \/ .* / ) ;
6061
6162interface ViewportSpec {
@@ -414,6 +415,10 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel {
414415}
415416
416417function resolveWsRpc ( body : WsRequestEnvelope [ "body" ] ) : unknown {
418+ const customResult = customWsRpcResolver ?.( body ) ;
419+ if ( customResult !== undefined ) {
420+ return customResult ;
421+ }
417422 const tag = body . _tag ;
418423 if ( tag === ORCHESTRATION_WS_METHODS . getSnapshot ) {
419424 return fixture . snapshot ;
@@ -748,9 +753,11 @@ async function mountChatView(options: {
748753 viewport : ViewportSpec ;
749754 snapshot : OrchestrationReadModel ;
750755 configureFixture ?: ( fixture : TestFixture ) => void ;
756+ resolveRpc ?: ( body : WsRequestEnvelope [ "body" ] ) => unknown | undefined ;
751757} ) : Promise < MountedChatView > {
752758 fixture = buildFixture ( options . snapshot ) ;
753759 options . configureFixture ?.( fixture ) ;
760+ customWsRpcResolver = options . resolveRpc ?? null ;
754761 await setViewport ( options . viewport ) ;
755762 await waitForProductionStyles ( ) ;
756763
@@ -776,6 +783,7 @@ async function mountChatView(options: {
776783 await waitForLayout ( ) ;
777784
778785 const cleanup = async ( ) => {
786+ customWsRpcResolver = null ;
779787 await screen . unmount ( ) ;
780788 host . remove ( ) ;
781789 } ;
@@ -835,6 +843,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
835843 localStorage . clear ( ) ;
836844 document . body . innerHTML = "" ;
837845 wsRequests . length = 0 ;
846+ customWsRpcResolver = null ;
838847 useComposerDraftStore . setState ( {
839848 draftsByThreadId : { } ,
840849 draftThreadsByThreadId : { } ,
@@ -850,6 +859,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
850859 } ) ;
851860
852861 afterEach ( ( ) => {
862+ customWsRpcResolver = null ;
853863 document . body . innerHTML = "" ;
854864 } ) ;
855865
@@ -1206,6 +1216,154 @@ describe("ChatView timeline estimator parity (full app)", () => {
12061216 }
12071217 } ) ;
12081218
1219+ it ( "runs setup scripts after preparing a pull request worktree thread" , async ( ) => {
1220+ useComposerDraftStore . setState ( {
1221+ draftThreadsByThreadId : {
1222+ [ THREAD_ID ] : {
1223+ projectId : PROJECT_ID ,
1224+ createdAt : NOW_ISO ,
1225+ runtimeMode : "full-access" ,
1226+ interactionMode : "default" ,
1227+ branch : null ,
1228+ worktreePath : null ,
1229+ envMode : "local" ,
1230+ } ,
1231+ } ,
1232+ projectDraftThreadIdByProjectId : {
1233+ [ PROJECT_ID ] : THREAD_ID ,
1234+ } ,
1235+ } ) ;
1236+
1237+ const mounted = await mountChatView ( {
1238+ viewport : DEFAULT_VIEWPORT ,
1239+ snapshot : withProjectScripts ( createDraftOnlySnapshot ( ) , [
1240+ {
1241+ id : "setup" ,
1242+ name : "Setup" ,
1243+ command : "bun install" ,
1244+ icon : "configure" ,
1245+ runOnWorktreeCreate : true ,
1246+ } ,
1247+ ] ) ,
1248+ resolveRpc : ( body ) => {
1249+ if ( body . _tag === WS_METHODS . gitResolvePullRequest ) {
1250+ return {
1251+ pullRequest : {
1252+ number : 1359 ,
1253+ title : "Add thread archiving and settings navigation" ,
1254+ url : "https://github.com/pingdotgg/t3code/pull/1359" ,
1255+ baseBranch : "main" ,
1256+ headBranch : "archive-settings-overhaul" ,
1257+ state : "open" ,
1258+ } ,
1259+ } ;
1260+ }
1261+ if ( body . _tag === WS_METHODS . gitPreparePullRequestThread ) {
1262+ return {
1263+ pullRequest : {
1264+ number : 1359 ,
1265+ title : "Add thread archiving and settings navigation" ,
1266+ url : "https://github.com/pingdotgg/t3code/pull/1359" ,
1267+ baseBranch : "main" ,
1268+ headBranch : "archive-settings-overhaul" ,
1269+ state : "open" ,
1270+ } ,
1271+ branch : "archive-settings-overhaul" ,
1272+ worktreePath : "/repo/worktrees/pr-1359" ,
1273+ } ;
1274+ }
1275+ return undefined ;
1276+ } ,
1277+ } ) ;
1278+
1279+ try {
1280+ const branchButton = await waitForElement (
1281+ ( ) =>
1282+ Array . from ( document . querySelectorAll ( "button" ) ) . find (
1283+ ( button ) => button . textContent ?. trim ( ) === "main" ,
1284+ ) as HTMLButtonElement | null ,
1285+ "Unable to find branch selector button." ,
1286+ ) ;
1287+ branchButton . click ( ) ;
1288+
1289+ const branchInput = await waitForElement (
1290+ ( ) => document . querySelector < HTMLInputElement > ( 'input[placeholder="Search branches..."]' ) ,
1291+ "Unable to find branch search input." ,
1292+ ) ;
1293+ branchInput . focus ( ) ;
1294+ await page . getByPlaceholder ( "Search branches..." ) . fill ( "1359" ) ;
1295+
1296+ const checkoutItem = await waitForElement (
1297+ ( ) =>
1298+ Array . from ( document . querySelectorAll ( "span" ) ) . find (
1299+ ( element ) => element . textContent ?. trim ( ) === "Checkout Pull Request" ,
1300+ ) as HTMLSpanElement | null ,
1301+ "Unable to find checkout pull request option." ,
1302+ ) ;
1303+ checkoutItem . click ( ) ;
1304+
1305+ const worktreeButton = await waitForElement (
1306+ ( ) =>
1307+ Array . from ( document . querySelectorAll ( "button" ) ) . find (
1308+ ( button ) => button . textContent ?. trim ( ) === "Worktree" ,
1309+ ) as HTMLButtonElement | null ,
1310+ "Unable to find Worktree button." ,
1311+ ) ;
1312+ worktreeButton . click ( ) ;
1313+
1314+ await vi . waitFor (
1315+ ( ) => {
1316+ const prepareRequest = wsRequests . find (
1317+ ( request ) => request . _tag === WS_METHODS . gitPreparePullRequestThread ,
1318+ ) ;
1319+ expect ( prepareRequest ) . toMatchObject ( {
1320+ _tag : WS_METHODS . gitPreparePullRequestThread ,
1321+ cwd : "/repo/project" ,
1322+ reference : "1359" ,
1323+ mode : "worktree" ,
1324+ } ) ;
1325+ } ,
1326+ { timeout : 8_000 , interval : 16 } ,
1327+ ) ;
1328+
1329+ await vi . waitFor (
1330+ ( ) => {
1331+ const openRequest = wsRequests . find (
1332+ ( request ) =>
1333+ request . _tag === WS_METHODS . terminalOpen && request . cwd === "/repo/worktrees/pr-1359" ,
1334+ ) ;
1335+ expect ( openRequest ) . toMatchObject ( {
1336+ _tag : WS_METHODS . terminalOpen ,
1337+ threadId : expect . any ( String ) ,
1338+ cwd : "/repo/worktrees/pr-1359" ,
1339+ env : {
1340+ T3CODE_PROJECT_ROOT : "/repo/project" ,
1341+ T3CODE_WORKTREE_PATH : "/repo/worktrees/pr-1359" ,
1342+ } ,
1343+ } ) ;
1344+ } ,
1345+ { timeout : 8_000 , interval : 16 } ,
1346+ ) ;
1347+
1348+ await vi . waitFor (
1349+ ( ) => {
1350+ const writeRequest = wsRequests . find (
1351+ ( request ) =>
1352+ request . _tag === WS_METHODS . terminalWrite && request . data === "bun install\r" ,
1353+ ) ;
1354+ expect ( writeRequest ) . toMatchObject ( {
1355+ _tag : WS_METHODS . terminalWrite ,
1356+ threadId : expect . any ( String ) ,
1357+ data : "bun install\r" ,
1358+ } ) ;
1359+ } ,
1360+ { timeout : 8_000 , interval : 16 } ,
1361+ ) ;
1362+ } finally {
1363+ await mounted . cleanup ( ) ;
1364+ }
1365+ } ) ;
1366+
12091367 it ( "toggles plan mode with Shift+Tab only while the composer is focused" , async ( ) => {
12101368 const mounted = await mountChatView ( {
12111369 viewport : DEFAULT_VIEWPORT ,
0 commit comments