diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index eba0f3d5523e..fed31fd4efa1 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -12087,141 +12087,153 @@ snapshots: scenes-app-settings-authentication-domains--boost-needs-upgrade--light: hash: v1.k794b7964.4a06b37acfcbb850cc7b9cda55f1b76d3f0c14cd9651e0d39a1dc250f14877e9.Qn4a4tC9pAXgk_SVych4jaeQT8CDnOIdVATh04x9ZC8 scenes-app-settings-authentication-domains--enterprise-mixed--dark: - hash: v1.k794b7964.9fc224431973adeb73e40c2fa1cd3279fc7bcff2999e3ad1788c47485b5d59b1.CKy_i4VpZtgrTZlXpnl3Y3LoYau3Q7bcPOwRsX36SR0 + hash: v1.k794b7964.555c2206d2e48ae8a1c8aa0e17849711622c183b481151aa7b13638aae2b9bbf.qL-u4tVhLVcCucBlvBeleoh2cRhMjIXHsGedS-Djpnk scenes-app-settings-authentication-domains--enterprise-mixed--light: - hash: v1.k794b7964.a2b4da24ccaf482ac9b5bd630d8433f2b959a4dfa208f921b6958ff6b572a6f2.VMJys96tOyheIc7S18DxjxYA5f0coXTaqheBBxAzJgM + hash: v1.k794b7964.cef5a9e9cd5f4e2d189b88c52d7cb5ddd0bfe653e4626e31ffc264e970dfab7c.11nCnyktZDcBjYjVTmJ-7IqJIsOBTzk90jAgLr9O-nU scenes-app-settings-authentication-domains--enterprise-redesigned--dark: - hash: v1.k794b7964.3e8f02d68f34dcff30b3243080ea1a76424759881b3f8cf9ff2d384015acda9e.RFuLUlHU1GxlCO0wWUShgpLEHiqQUb4HgXm0UiYKMv0 + hash: v1.k794b7964.b298edb9b3a47c3d168805fcf8f89299e20a8c18dd3c92b928294d18d2c3a99e.-gtvgWyq1_SJFiZKdZRABzJh-kc3nM8mSh54hCzUQ4I scenes-app-settings-authentication-domains--enterprise-redesigned--light: - hash: v1.k794b7964.00b10bd2d30f2eed42849071a7bc4c17c83fd3f676d9e18bcc556ec3ccf69f6b.POhcneQklyAcMsNwfE0xOFs1NdodTBRMdnKOYYDZ4iI + hash: v1.k794b7964.e70ceb58d43f6ac38362ca3bf5ae8f4b807c37c443f31a88109297576a1b916c.2K-w1xgTEshEMfliSPcAzt9BVmrUJbHSX1vgIGnEBG8 scenes-app-settings-authentication-domains--enterprise-redesigned-not-configured--dark: - hash: v1.k794b7964.b79cc699cabb9eee0de19ddb9c5b3217639c35022da22215ddf3f513c975ca63.SSYRK-UcoBNAv7r5Kjfz6i2gbXAsqgXSWXj7XnQ-NXM + hash: v1.k794b7964.7a8f70c3b240ecdb252fdbe5a57f5c4164d62c99656251d7e295e4fb6598c1d0.EpR8zcTtuwWC00nwxMplgRBFeGeHYEhBqrwkCdv3jGQ scenes-app-settings-authentication-domains--enterprise-redesigned-not-configured--light: - hash: v1.k794b7964.eee3200fec6cd00cfcdd44d4f91f91cd0bbe45dd9a7b6b14cd29adfc60cbdb84.3bbaGntKqYr5OaLg8P1TW9rY7b1_Oi5vM-VtY2xBrAg + hash: v1.k794b7964.31edca1580e038cee78b2c800c71a039e3ff1191817a250340fd0dc360d30c47.GbHvlnpRsMYhtrDznK6H5IsC__B--N9es_s2vL0P6WA scenes-app-settings-authentication-domains--enterprise-redesigned-partially-configured--dark: - hash: v1.k794b7964.6f49b4b591a40afcefd8b68e6ceef192aa9a2dfaa85f9bf134e015e47cc40f31.gVgaA3rK8Ca_ECCpzCGj8UL_C9p4nz3vNBu__t0Rq6M + hash: v1.k794b7964.0995a71abe163e2a9fa206b26e677d509dc4d95050ed8279b823442ba2219d01.G0HxQhWm23iReV4Oyh36gPLhWh-SxPuw1p7fbLkMB7k scenes-app-settings-authentication-domains--enterprise-redesigned-partially-configured--light: - hash: v1.k794b7964.2bf81837e241e978315801d00ec2091ac00f82aeb2f78d792eeebebbde0d669a.YrhtwLygEq_wVQvTdl7s1H6Zq6QNTFl0nH9aRrB_3Bk + hash: v1.k794b7964.654074284f5d42bcc912e59f093898155e0e00889258c6255cbe7c0452b29316.HMENKjNHfM1zg0MgJwEPEzkUd_z5KFBhNKF1zXrQLfI scenes-app-settings-authentication-domains--no-domains--dark: - hash: v1.k794b7964.597ad7ea8dfb77fbcee778ed41f05580d86046fef2ff784573b1ecd6858d76dc.kw2uXzfKVCC4p8TZS2LlqzhOMKA-4Q6X6tP6efOlLuc + hash: v1.k794b7964.fe09466cd97f88923bbda820575ffad58ea09d42758711d3d04352cb4b937f42.aJXkbhe0FeVNNx7Uqkb5yul6d2HKtuatm4Wa7FEbV8M scenes-app-settings-authentication-domains--no-domains--light: - hash: v1.k794b7964.e993e7fbf0da78950d690652ab996f3a18056585b65bd4cc28d173d2f14ff414.27THupjpgZ2fvcrR8SUzJiWAJu-pvpriGFUugZ5CI-8 + hash: v1.k794b7964.5250c772e72f4a92c0b5ad5cfff960ef0225f5d1ae90c7abe2554707078918da.fLnZiKz1IeSuE7yJP-xjuETbgKNRYOK6wC97HQaspig scenes-app-settings-authentication-domains--one-unverified-domain--dark: - hash: v1.k794b7964.e68f7ac60c73fb0779f28e2d37840e2f37e86086835b7bb0a4e0ebf11b0da707.iydJ9p0YXQjCQH10nkvyEiuJqGwoaq89kM4afi2zhMw + hash: v1.k794b7964.4829ceb1ab4d8b9af89b8a5e8fb8f3cf9e43b6a48f829f7f94bb9fdab47e0e7a.iVk3ZeZX0c19wCD0UEJShVl5fHju8v0EjumakzyFf0w scenes-app-settings-authentication-domains--one-unverified-domain--light: - hash: v1.k794b7964.193686f0a48bac9730157515b56e3d02c8386150b5ec2709127c2661688f7f68.5AEa17e3f-bjiIqLaTfT4_BPeT7kZB3a_FTldUf3unI + hash: v1.k794b7964.1df3bcaa2f27eb76c94e6cb99c115806c71c4e58aaa58f5a1ea96a09428344f2.z6i_4adj-YPrPkYhnYqhyeHLZ3997Nsc6LIAenqhiCU scenes-app-settings-authentication-domains--redesigned-needs-upgrade--dark: - hash: v1.k794b7964.c9b61d174f4b8d1ba93b54376a930b4820a2d691909a68db19eaf128866c903b.fcTXOGkUXwuZviQXUWzARYj-NA35NYIV0Dys00tw1iY + hash: v1.k794b7964.1d1b6a32f9ead543b632d497dc7dbc34f5b96daab7ad9b5c0f4b3470770009ff.WbtbFLq-gVN9Z0pCnTbNxcD65M1M4SzoyI_GzC7L1Ys scenes-app-settings-authentication-domains--redesigned-needs-upgrade--light: - hash: v1.k794b7964.3796c40507d8a070e703133a44893f18ea1151ac7f2cd2c917f39229abd92ec6.vV_wR2Q71Sqs2y7afXKOx5Q3PJ95YajviZzkuF8OKZo + hash: v1.k794b7964.5a444beb49672eef574867a37195f15caac349e4d376be6ed332272ee96668da.VqbffqNYSTbF5nkcvVuT_YlJlC7PHKOgqilzmn65QB8 scenes-app-settings-environment--settings-environment-access-control--dark: - hash: v1.k794b7964.1f33790f3bd0e99fd788a2ba51b5da89070890668342136be956671fcb7102d4.CTModQEdtdAl7A8abkUSFmeUOK1607H1sKFdQhJBq4U + hash: v1.k794b7964.51514ec8b8e6cb68163d547ea729423a0788a5f3ab3608c4fd618b129f46269e.l7rsRCKUDkjx5sacM9lblcoo1zPez0FiFIMvI-YHmLk scenes-app-settings-environment--settings-environment-access-control--light: - hash: v1.k794b7964.9ea484177cb94c831f0508e3482b1a876fc2b69db66c95b8f69fe73586909a3b._EvRaax5KzNHywyiNOpnbtVEPebu79kfKuX5zFMrXj0 + hash: v1.k794b7964.4db0b294055268042339567332baacb7e146a3e9dc23540758661be827676453.-yd-uoKdLzSw45djm908SeMJ9Puv3sFy3tLjMOZ_a7Q scenes-app-settings-environment--settings-environment-activity-logs--dark: - hash: v1.k794b7964.bec25f039acad0509dec5b2fd7cce474115b8392e538f765e4188759d7df9e4e.tE7Zfs5dnNod8DYmAb5I4E2gvTd_scj-SO06DqiIHRQ + hash: v1.k794b7964.77e05246b50e53a5056f2db5422000f3ed3bac2572fa52ae4dbda2b4caf892cc.fPIh8WWWeaBdv_iYhbsFfeWtDdqILqXtOO7qiCVHFJM scenes-app-settings-environment--settings-environment-activity-logs--light: - hash: v1.k794b7964.6ce5b161e9a5c35ca3ccbcf3285a13dfd9cc4b42fda92a347502d426001524ad.Uj3btOOX8b5EqJL4mIgv2-2A7WVTqC4ivyjuytg7xv0 + hash: v1.k794b7964.da4902a10ec85ede9f33d440bfcdf11be8fe8fac99fe26812a812262cc66cc32.TNhTGor71Qhsl-6OJQDbdl4Vy8A_rUd0mP3hAULxmd4 + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-connect-modal--dark: + hash: v1.k794b7964.59a30ca37b4f93272b2b9b44770762f627e85473cf9b6d9b1317f40d1e3a386e.9M8kJYzOFpn-S29wqrQs-FQafboMhKIh1baDmaXrvGQ + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-connect-modal--light: + hash: v1.k794b7964.cebb2c0841f4e4e37d2ea1df6e0104ded1eb59190a5c4ac0b69627f8fa8e3c6e.XAjjATZlE8eh-QyykaJWux_ZL3vwbQa4pwIV508e_yc + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-connected--dark: + hash: v1.k794b7964.976e2a7be8230b768d09c36bde11f83e6b36ea82962f7bbd8154ac4a6a399f0c.QC0Dx4iTFeEi1QKSV577cHD_WW1cR3zm_fLQWSM4g9o + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-connected--light: + hash: v1.k794b7964.98304a689c136222d75db956f1fda4abfc4cbf1902ccdb4e68c6023517e81fe5.oizhDuSgd51ralAxt6MtCBlh3A7PthmgYRgxZkKashA + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-reauth-required--dark: + hash: v1.k794b7964.cf7ad6f544ab4a991f52e0ab9937a9409810b15c7b5a1eaa86f40a3c0030bf99.AufyKh0bQX8I4CAzcHrHIjfoKhw6w8H_8DUGjdYTcuQ + scenes-app-settings-environment--settings-environment-ai-subscriptions-codex-reauth-required--light: + hash: v1.k794b7964.8120cda1fbc968f256552be022afde2bf8b58b102770a61eacb80bd96eff4b22.yUyz-CBanhJ6QvG7rjgQ8Ku5XcSfb5kBtRBzSxlmDNQ scenes-app-settings-environment--settings-environment-autocapture--dark: - hash: v1.k794b7964.e92fb6c59f611c46950a85444866f8aae03fe8c6713403fc0e1fa090a1d3471f.EdK2ioxkG8wCE9caWvvH9fCfVfUnRBOz97ArCisFzEI + hash: v1.k794b7964.367793a59557b95b8aa86c9e00c3bcc423034d459da8b28289a49b7bd15cea0e.Z-at8v1BnBOViS2ggWfC7WrFr8Q4e8hedP0TaSy0HMg scenes-app-settings-environment--settings-environment-autocapture--light: - hash: v1.k794b7964.686906b13609b91551ae24ab45ae35b172b2bdaaf66eabd46d40fb4c44e4bde8.7NAOH85bpCOMpDiFOYUGT_oRXb_gu1FO53YJNnRHHmg + hash: v1.k794b7964.6e179f57d058675781df8d0a2eb745eb63c758cfe4e022724844c924535facf2.9eBiDedYyGXg1fqEl3QvodQplHWcurvti0dpcq8f7Yk scenes-app-settings-environment--settings-environment-business-knowledge--dark: - hash: v1.k794b7964.d06d7a1e940bcc3f4ba94b76fa9e38490de7d4d988e74dcbb351a432526c4a98.vLpD8WfQwYCeG8f7-5qkX9CRF6K0R_B-n11w_3ktQoc + hash: v1.k794b7964.6f5b2e115f2cc6dc7b8a6452f84b94b41db19957c2756c0046ec279c7f0c26aa.r263b-sZfD2h2td7a_Olau3oMpXhoFDEEWPEfaZIS0I scenes-app-settings-environment--settings-environment-business-knowledge--light: - hash: v1.k794b7964.47f7934649c8797350cc2e0eebae70c610af4647443f65751dfb71bd23ec38b6.kUkguqRJpQpn2W2B5labUVj-doAvtEJHvUOHvXIbVgE + hash: v1.k794b7964.c063ffc67747825cc2f60c85e798d5d8495e580797b3797dbef0ae287dbaca2d.G92ATWSQV7kwSAceUQ0WI-MS-A0NoTztE8Ba6wAUrq0 scenes-app-settings-environment--settings-environment-business-knowledge-learning-on-support-off--dark: - hash: v1.k794b7964.8841aee06adb4196acee98aff7d7c4e6733a206bf5a53d51558bc3389842c067.Zv6iCHrrp0QbdkZoIAlBBGJNCAOH7E6vz16AnURIeXs + hash: v1.k794b7964.2bc66b383ca9d5af2ece56122ff0228f0b4fe80bd51a7fd33e5c4176fed9e184.Bu5ufqt73vmenZT6gndiqHB6xfsrDsn0AVttB0dHTdE scenes-app-settings-environment--settings-environment-business-knowledge-learning-on-support-off--light: - hash: v1.k794b7964.6d2bca782ca9a82046e7c642dac56db988d6c28b16aa859896465ed00aba4ad1.eNC9cW8gJ_QRhTFqDqMrNwuYfc37CishASPmjsaPCYE + hash: v1.k794b7964.e6e896054d778d611ad4dc74ab94bf0e67d6b94df64b2f0c78a647019972659c.8RDaLTHlAbq9epWNARvIuzfOGlpnFFKE39zmVDWOG3g scenes-app-settings-environment--settings-environment-business-knowledge-support-off--dark: - hash: v1.k794b7964.bce172a26681b62dbd5a19c0ccbaa40ba06d8d2cb89fda7f910c405aa272510a.II1h9yvLFH5FLRScqz8Z4QSDoXMYgKDk1rBRs8llSwU + hash: v1.k794b7964.693f5f97cb54cd1a09bfe04679257faa2c566c0444427704b8eace167b718ef7.L4BQLvcBuEU6XLxRCkdABjiwyFJEpqZbFACaKF_wdDw scenes-app-settings-environment--settings-environment-business-knowledge-support-off--light: - hash: v1.k794b7964.5914363a6ad04d1ed33c6a2c0a81ecdbba3f072513f42dce7a81e496ca2ecc42.Wnq_R_QLNNQoi3DhmrykBnqzS4JhNzpR0uSlHXWj3oQ + hash: v1.k794b7964.ff1f7a5f094b835173fbb9c9e510bee049f273ea66ebf3d17c6d6c806ef045db.ijUfqrqwROWgFNcSmbA3YZiNDGZ5_HvGEyZFzg46fiQ scenes-app-settings-environment--settings-environment-csp-reporting--dark: - hash: v1.k794b7964.c3671c268239bbe4e0bcedaacff16f98b85e7a07d5b791808ae5690b3e18219e.ptL0237qq5NbB36qZvERvPgvs7xCmXmznF-OtHd3ZwE + hash: v1.k794b7964.4c318af389fd28ec0004bf8f1bd9f3a8df198f86994b1a7ca3a83ef2d3d96508.7Iv2WfEscRCn0pscJy_AziNrRI9LbFxu2gBx_Xq5neY scenes-app-settings-environment--settings-environment-csp-reporting--light: - hash: v1.k794b7964.7987a483420b6d9c60a27ac3bce58a6dd63c80af7afe25f9bbec7b240a757fa3.Tsa856gKfU-Eu93BcLs0x1Xapszlv1KI72Tyy0M5lXw + hash: v1.k794b7964.1ff194ccead2e57a82b897ab93cfb6583d896230a6d5ce92bb8dd1ca84d0f333.HMd3aaZlKvNnrBYCsOYvY6YAk-bCG6GvvyLTOBsp1rU scenes-app-settings-environment--settings-environment-customization--dark: - hash: v1.k794b7964.ee2f6721a73ddb3d7039f049aae8875dd708a66f8216c3920cd5067daea467b9.EQcyIzhJH2q7_T8ljDKQh68EJBH4IvWyoxUNpCEcB4E + hash: v1.k794b7964.71795ad88959169a3a4a17f6ce92757a2292766eed9ed3a3cdbaedc8affdadd0.W13tXX3XY5iLme-dqIJoHTaWUscQ3G2FZMOE2lJdJqM scenes-app-settings-environment--settings-environment-customization--light: - hash: v1.k794b7964.6ef28ce703b233b976d1cfc18beaf438b4d42dbb2d24d2c74f230506d7dfc85e.iTiNSq7dzo6FXo-0_mBgG7YRg7SyE2oHEd-gbxXUWko + hash: v1.k794b7964.a86c67fb8a8309bcf08260f5e5b15915800bb25e3fca5c23955a4f42565c7d39.F3We1kolxBexWXl6OM7Cp9HcFldneFiRMUEKhBUh-AY scenes-app-settings-environment--settings-environment-danger-zone--dark: - hash: v1.k794b7964.9c2fc7dbe005a63e8bdc1c8d1886819cb871b71d95b33aaf3ffcca61e20b85d6.j59FsmtM5O0WRwbSokQt9pkoz1_xfHpAZTLJGL3wLU0 + hash: v1.k794b7964.0e2ec0c2fc1ef05a02c0e46bff83e579af24f1632c149e3dd9907ef0b2d7cebf.G2sadBIMXSkxfggWLDlEgGoPv7-JkZCn2Cmp3MIobwc scenes-app-settings-environment--settings-environment-danger-zone--light: - hash: v1.k794b7964.a9d674f34d4e8cba39f0e063953203a8cd3bf3ce17518bf4040853331e6e45b0.tBQ8J_bXB7L6PQifxRLFP74EKRbDMxR6V_75GGP0loA + hash: v1.k794b7964.daa4b6fda1204424af33b68386dd2f411913d2bff879649f10bcd7d0ffd97012.9RZ2J5doNNHWnKBAVAKl18XUcGo2Y9Wi3-M6rAPF1QM scenes-app-settings-environment--settings-environment-data-quality--dark: - hash: v1.k794b7964.abc8dab1023d49d442af53e4961c2f82fef74b58333ba1cbc31475c6c543eeba.JDVZT3bK74d6Y-ghPZiHtZ_0rmBhVQlSQyzZqIrnEPo + hash: v1.k794b7964.e787751125fd3d8257fab26194b6fb067fc72752076930dca722708bd0c8a523.cTMdf1l9jK2GW4535c3s8vl6Yr4ce-UpK67K8lDrr38 scenes-app-settings-environment--settings-environment-data-quality--light: - hash: v1.k794b7964.55c4bbf4a7be16dae70d6ce8a31d8a77360b4b1fc672108923964940fab1fa7c.-QhpibJDRFWBAd-bTKO0YasIrrWF9DwnGdvBySklouY + hash: v1.k794b7964.18a9984d2afd895eca4e5011bc2d5b12b78e4ab93941a3e953ea6e4d0221432d.jddOYxwXacmniacKkVg5tCf5LrV-Di2rUOTf8q3lqhI scenes-app-settings-environment--settings-environment-details--dark: - hash: v1.k794b7964.9c2fc7dbe005a63e8bdc1c8d1886819cb871b71d95b33aaf3ffcca61e20b85d6.ezjk5Je5j_3QaXbKCIeQ_IrRb6Os50bVwFJDw87G7AQ + hash: v1.k794b7964.0e2ec0c2fc1ef05a02c0e46bff83e579af24f1632c149e3dd9907ef0b2d7cebf.dPqwrvg23WMBb7Qs2-L66fOgLfc4-zw23uoCK5l44Fc scenes-app-settings-environment--settings-environment-details--light: - hash: v1.k794b7964.a9d674f34d4e8cba39f0e063953203a8cd3bf3ce17518bf4040853331e6e45b0.Ha0bPYR869tF1qd9GEb8TYRrqW4XJ24YkKgfBGVqwDc + hash: v1.k794b7964.daa4b6fda1204424af33b68386dd2f411913d2bff879649f10bcd7d0ffd97012.4i-jJMkFLLuLTbw47-UJB8aVut4Vt5tiPebfteCUzM8 scenes-app-settings-environment--settings-environment-error-tracking--dark: - hash: v1.k794b7964.7856b68dffef3f3617b0f3668d094522e55363a3f63bc4fa12f9a4ff75a948bf.vEkS9WNH6ExEKgAb_SGccT6IqEpzKBsYRJN2l02H8-o + hash: v1.k794b7964.2f1e1ba97d105b4c54d974d6acb8da969f01d44dbdef88099e3fdea2f9def4b0.khIC6RwEG11ygzQwuwrZjC1n9WgLytEf6TjJeMDf-Tk scenes-app-settings-environment--settings-environment-error-tracking--light: - hash: v1.k794b7964.ab7b4b74c3495b5e9d6152794cb0ada24201dbaf00ded21fb1a68f884ffe7139.Wu-cGBkxQLcGE6VAu_gpYL2pFrfNWHLAFsT3jutGEf4 + hash: v1.k794b7964.d735ad69076ddc25adfb11df95c3423adc843881df4cbd8cd377bef787aafd5d.veAC16dMWtKzR_0Xhfqs8bZY_gVN_77U65k9L47_Sj8 scenes-app-settings-environment--settings-environment-error-tracking-configuration--dark: - hash: v1.k794b7964.5389531656b98551428d0348f86123184edee37f4c3a32fdb4a1cc0869adc3ff.W_v_OXNRYXMUup9iAKFHeMPkwy3iwVpJ6BlmnA6QO_c + hash: v1.k794b7964.ef47871a76573bfb68a1be8be6d6c015b0b94f04155d4c5e6398e09ffbe966bb.BZBmjXxrMPoITiUgIJbkmzU115N2l2K5w9GEr8NY8F0 scenes-app-settings-environment--settings-environment-error-tracking-configuration--light: - hash: v1.k794b7964.05cd4d11178a6ab2b511d9bf210abe0d975216c98945669170f00138e560fd16.sqlVQyLt-1PLj-fF9YbPdwnFo_RxtHzrHyZe2OQiSDc + hash: v1.k794b7964.e8db2dfd2fb715a7531d23c25f4980d0b1bbf5bbed2282e9c088cc1832b606ad.I-DsD_OJCyMYElyPt4G6MR8Jn596Y9ocvFR32Non_JY scenes-app-settings-environment--settings-environment-experiments--dark: - hash: v1.k794b7964.34ea9c4a5eec940e7e9a2a9435da52e1ebe91d2f7a7f540c76b6323bfdcf830e.HzYgBbQwQQww7_CWcnVIOZ5AwNvp-HlX1gxrQsDwoa4 + hash: v1.k794b7964.b77940e10833497f2934fa666b818211a461e4cc23968eac8081ffa492a44871.Sy3_XFtYad3CXtP44eYYecM_FkNZxaK6yUbImsfHDfs scenes-app-settings-environment--settings-environment-experiments--light: - hash: v1.k794b7964.50405eeca5576a2c771427b596d742f2925f63e17bc96558f7969c65469fac79.yoc74JSxnEwjvd8Qneqn2pDzNMKiG5sx1D6LGwoe1Wk + hash: v1.k794b7964.b67f27a32dcddfba204110607badf7bd69bb81414ff23bbfba45a64bcc8fcf78.cufAjsNz_0VwvxA-ym_S-33ykGm3RIBTEC0eyjMyu2c scenes-app-settings-environment--settings-environment-experiments-multiple-recalculation-times--dark: - hash: v1.k794b7964.5b034a5effb472ad3ef818ec75b6b9506e70d95810aac035050535713157e1d6.8do5WxsjD00LvHsn752lDv_sH9FdxOYyPZM7AUzdDqk + hash: v1.k794b7964.05aefb06071faccb91cc9383647db6102de2cccb2545712dc9a18fce232423d1.E3UBcV-FMc3IV6I6_7Dg0oHEoxl1dyS-fSaAGmJ9Wyw scenes-app-settings-environment--settings-environment-experiments-multiple-recalculation-times--light: - hash: v1.k794b7964.fb1bcb78d3098e7ab06fdbf1c2f1902049de9030d3769520e78851d3c96b7b24.VhR7GZo9z3_d4yiKMeBu2-54QRDyq_8jIOqlZFwDG_s + hash: v1.k794b7964.b0f58bf8807f75ba130ffcbdcda6f1c5c176a753df9d31eabba48581c22a9bbc.GJ9_gkAtzF5luUTSsKAp757jQGcY--pFC4pXGtg4D70 scenes-app-settings-environment--settings-environment-feature-flags--dark: - hash: v1.k794b7964.f542dc208f8caacdc2978c3e17d6c077d299f4896b6acf1b0cbe51eb1e7e2f34.KnYG2YFjexeGzYr2iZlQmND2MXxJXDH3G9_vOeCMy84 + hash: v1.k794b7964.58a1c19d0dc5e0d260f2595e22f7c5663b2eaabf8958f197f93f13d5d49ec351.-RCuviy-hN0AEieJrxh2jJXAcyoqjJbvnomoomo-H5A scenes-app-settings-environment--settings-environment-feature-flags--light: - hash: v1.k794b7964.1727c58a0c3d1d7b98d9837e7bc9371ed6917ece4e4bd56e117c8346dcdcab73.33HDPAg7mL_qrKAVTV2Q9hGSZxtDS6geLAt4ohn8Uw4 + hash: v1.k794b7964.6f80a164a435dafa0d5b703b53812d8e299fd2dfd25436f639603d9a7051d5b2.51FEO24muV7mD6nOLcxMidknhNGRNVKHsBCkmRBG79k scenes-app-settings-environment--settings-environment-heatmaps--dark: - hash: v1.k794b7964.2e1a4ab99529aea8bedf4ed8acc2a6a88f82070cb66efbd86be237afdb76ac43.r49rtZmhxy0F163z1LlyTV0x_ZBOHMT03jA4exX3kks + hash: v1.k794b7964.9d0590199b32121935a14ddefede6fa3b5350852f656b82a271ae10b3e6cffff.NeTZj581shyTlCO-mM6qd3ako_qdcm1qMDRcOqa8kvg scenes-app-settings-environment--settings-environment-heatmaps--light: - hash: v1.k794b7964.7e0d0d75dd8062ce9a721d0c02670e8296719c81920cf734714f01acf2acc8b6.W-OPwcwWXMEX0tktVzfYANAaZCHh-AHV_do0yhmyFF4 + hash: v1.k794b7964.893e0f237a290f6528720d7e4c86567565fc6c267fc40949df83e4d5b7015155.WuCzucaZbhb1khJcJh8qWVqCRU21x-y-79Gypj52PRE scenes-app-settings-environment--settings-environment-heatmaps-screenshot-cookie--dark: - hash: v1.k794b7964.21109a9420a75754051c04b640bfd86af75f7461f161082f8cc47e4e46a8bc41.tkqEvQIxePpDNgTj7n5CJ7opSoIwI9DQO3T9Eqjxkls + hash: v1.k794b7964.91d3dd644cae697882c2bdeb34d1cad088e14a57e4cbc4cf559e95e9d101d0bb.282lL7DGqJ3f4gGP3HIfEJygIt14EvP57hL1tnn-TnY scenes-app-settings-environment--settings-environment-heatmaps-screenshot-cookie--light: - hash: v1.k794b7964.bd06b0f161066c3188aea067ebfce219b05cbe333d196f68db83080fb6807c28.prEHUClGQEvlS9eF131QntF_FDeP0hSACpGbyPYfntg + hash: v1.k794b7964.2fc452290c2db066bf62a169bc0184d79c4b50592c9ce48d5d70321d17061e6a.OOU0HChlFXcqse0SxQaI05mGYjbb1Iu6U3LN7euvUaw scenes-app-settings-environment--settings-environment-integrations--dark: - hash: v1.k794b7964.8bc9a0a3c68e9379000d45e767214553c220039f07512af99d61892720b7d9aa.kDGcZ2uuaPxJOJuTXS2lAqe9TytcYSTv2liWakub61Y + hash: v1.k794b7964.95cb5dadb923df180feb7d258e1d555847b13fdea54037e16ccd35ad87e1d17c.7XGtCXQYbBlOMoYFTC1u4UIB6tc5OW4DmpA2GaPmRX8 scenes-app-settings-environment--settings-environment-integrations--light: - hash: v1.k794b7964.7e44eb5f34601ea71f789a14566d68ee8cd899515623d56eb7ac5de3b4a298c1.BMEcaP7cAAXuIFiiqvKbqlZDby1H3nJJ93mBWy9pwko + hash: v1.k794b7964.19116f57a291558ee81b08364acb1c30a67268886c226e53bcfc8bc67254f41a.OYTq-h7d7dXyjjcONOVAb84nP_TVFYf75QykrAMW9rw scenes-app-settings-environment--settings-environment-marketing-analytics--dark: - hash: v1.k794b7964.ce50eb2ceb93107a2dd46a661a1f4a746e602f968b55216af427403068521729.viGh0iVO1vMHTAwXpJEgQvynV4_8nVe3mtmKBwQFR2o + hash: v1.k794b7964.effaeea9de7320e928ce54f05ca50cff81d7d6b0b2fc1637fdf0fc84359c581b.QHkITko0zLZbGJ44zX3iYZWNzzMgWkNN6TvogJvmbZU scenes-app-settings-environment--settings-environment-marketing-analytics--light: - hash: v1.k794b7964.dda239f4dc08e45f0522ed95556a3166a6466a101e0a6cce55eec0b106fec096.bOEE2G1h8ZdInRkqU2kqSDWFvej--F7ETK-vXit3I3E + hash: v1.k794b7964.aa8c68f8349be316d5edfe94138d13ce993a16ff0572005a7bde5bb4e0adbcb2.6yuWFlAOM3oPzO6i0iIOvJ4CthNxRnzux-mBkmtnHEM scenes-app-settings-environment--settings-environment-max--dark: - hash: v1.k794b7964.901cd93493c9ac78c9c2a3920c57712a363937e3ce82450fe596e90fa94e7bf7.XbiCnsj2Ilnqu_UdEPfQvUlWqUXH1e5jVecRvCuPkhk + hash: v1.k794b7964.3ee81995e00dae2ac17b3fde2a16b4febcc4745672bd73490288b722e75a3346.W-Q9F2VG8vWtGDLW1fbrtWrM2M_0xRpK_68oDgkFoLk scenes-app-settings-environment--settings-environment-max--light: - hash: v1.k794b7964.eba690b86e7d5db603f62599109e5488ef4b3578161c35ec0072e41abd81ffd2.2G_ACagN3Ma52M7Ll4_OLvNYF64uyNt7OFdlQHDaliY + hash: v1.k794b7964.286009ea57b1b8dc6b2bf565afb038ef94103fa736526ddeb14765b877edae31.lFGAY0CsBCuCodhW_7iIdRFD3ofpmbTVjsa6UM-aiq0 scenes-app-settings-environment--settings-environment-privacy--dark: - hash: v1.k794b7964.d8e586dc15bd266a7e613d061befae332c00b43233b51dd7136b10aa1852b25e.LjZJ8qpkfWzAWTt0RQfxynISD5btaWsqI0BcdiNySis + hash: v1.k794b7964.653af63418c982df3d266806bff153eb3364aa35d63f98e575c6fcdaf7cbbc3b.EyUi0cvY7MMJZ26zn_TMlh83MtE3aQwUh-iTFGfhZWE scenes-app-settings-environment--settings-environment-privacy--light: - hash: v1.k794b7964.15ff892804a4ad351c4d70e8080fe61fc0da06fb305c71913a4d5236454a14ae.N8KJjDbitWNv9AoP99vHJJinCEv7_zfPaxNhSu5qL0M + hash: v1.k794b7964.fd4383c73e662c1eb947b22598cdf47602ba411a5c8937e14e56084ff68223d0.5PNeEDM26Nptckt5nTk6ckPwog7FHUKoNhnRzjLEohs scenes-app-settings-environment--settings-environment-product-analytics--dark: - hash: v1.k794b7964.35ee5692373e5eb06d01513a297e48c98d788004fd1701384c6a5c0c3db08fa3.slqOWsf5Wqr5FTWLPZP8OAvz5rqOWRpeZFjxnPcc94g + hash: v1.k794b7964.aed5f7fa9160d3eb9b64ba2fa71ae7892c6f15f169a81a5a14d5009d7ebaacc8.WailAz4OXbLkd51EshYrOERMzayBmA10Jw-Vyq6MKX0 scenes-app-settings-environment--settings-environment-product-analytics--light: - hash: v1.k794b7964.1b283e45e0cb0186139ce74d01e941f770a6de73561debc73693ac4f16376a4a.kpj2Wnhj2GBS3G2uP-4NW45LQBMFmVYKFG0w6OdBci4 + hash: v1.k794b7964.a271142f12fba44433f520c027e90c8fa043f23529f64e89080e5e49d033d710.tk0LCHNgXU0PA5opPxyYKy9ZCz0yVT7f12x6cY1RlSo scenes-app-settings-environment--settings-environment-replay--dark: - hash: v1.k794b7964.31cac30c899ed647b34bb36014bbbda7d11127c3055104e29f7fb324e344ae6a.aZgb_cGTDbZ-qtWSSpM2FfETnqtIxHxw_2Ns0wc1aGY + hash: v1.k794b7964.8eb2299f4a7b8efa2d3e7455fced7b207586e109e3e2d4c817b999031dde2028.k71eh_hfw6M81gbrDXb4o0_ghTOMYuc283fYqzE97yc scenes-app-settings-environment--settings-environment-replay--light: - hash: v1.k794b7964.e2a4fa6a1188d448757ab298da320c0116c3ca9b359512e649026320a2a78272.pTJGsfjhPFRazohsHqKC99stkbbkwZUjxuBLSbsrv-Y + hash: v1.k794b7964.4b85566b5497a9498c83ad146a76eb60a435fea1518e59b6d96c902aa612dda6.z1ObtBYXH7dyyS3pWDMsTJCs6sdLH61DgJnAK9FJVyw scenes-app-settings-environment--settings-environment-revenue-analytics--dark: - hash: v1.k794b7964.3096de7441604189a012773d4fb69962b4c7ab84e9ab85ff446b6ea01eb3d3ae.tbcVbVcyeX4BePfjP987c7NxFBW60_eSdYMJ9TGFTNs + hash: v1.k794b7964.857fa4d2d31c457ded8ffd0d7a46297806545aaf6156530a08e33293106413c1.CJIonIrQ6EUjj8MFip0N1vlRvcHYwzlZGD582_TvTLg scenes-app-settings-environment--settings-environment-revenue-analytics--light: - hash: v1.k794b7964.3aab8c17526a6524f1b53469c26bc87d593485612b07253612982f921837bedf.B6_zzcgOYfXX3D8AlUdX4oNSnL0M_7GdtqrgRZ8ThLY + hash: v1.k794b7964.88818effdbc4708006f0490e5c852f9f5d31392671c8ec00769dca33b69632ec.EkOX3q_2hOOGE0VDnUqEItIr_pQ1cqONQMaccGUpZu4 scenes-app-settings-environment--settings-environment-surveys--dark: - hash: v1.k794b7964.a39a0d9b1097794ff9523add1ea41b366acbfb28136fc35b592108a3e1a70e01.OIWd5RSEQshyBEE3g_SH2GmNJ4nJoLIGieAwatFQtT8 + hash: v1.k794b7964.55c2287db5cd6b2cb9e140220e47e3b4751950499a347405a5bd2c7558341480.sdH80ULUewwPyWi8J8-DD_Eb4ywPZ6hqPNvD90z_5Ns scenes-app-settings-environment--settings-environment-surveys--light: - hash: v1.k794b7964.163747e5c6e8948df4db226d98285ec6dc82597df4b6ea3da702820d623813f2.Uk9gJMmhlUE2FyoxnXeMk2BrQb8AZbcRRWI0lw5_ZtE + hash: v1.k794b7964.a8cceae328f30f76b19a51523a9955905b03d1b6b20239aeda5ffc8dcfcaccfb._bbOOTUDAdu3GXt2Sfz5KOSaPSQujb4AaBDo3MnyTY8 scenes-app-settings-environment--settings-environment-web-analytics--dark: - hash: v1.k794b7964.fd9812c83ef1776245e1cb1c4ef2ce48513ef19fb473b7f4869348dbfd48370e.YhBCAewkxhTd4nVZjbokNjzpHk5Bkx-U9IefgcSa_WM + hash: v1.k794b7964.62cee63e340a94e0318420d76f4c606b74fceb074cd536d5b938969abf8d0c2d.mceb4JDI8akjSC4_y7hiK8C_266SQskkiATxBbkYKG0 scenes-app-settings-environment--settings-environment-web-analytics--light: - hash: v1.k794b7964.71f6b9c1343a9bb2d8ec78dfebae8d7d9845882353fd2f03f63d45c5ce238431.gnx48GKk7hyfbiZTB-hdcMC4K5dpCekR-NpE7kD7sVY + hash: v1.k794b7964.36f188870df619c691b946ddc92e2ce5b9a9c00d87ec59763a18afbf07e6bb77._Ng9GWIeJmSHwsoR0taQckukDWegPeUxs87q_GoqKAo scenes-app-settings-identity-provider-configuration--oidc--dark: hash: v1.k794b7964.1177527533a6ec000b396e1ee39627722226aed5cbab11759198dbbc2d72dcb3.NqKVUNSdSz-yE-r1M5EiEIaPRbQwBbyjCAf7rSSi9Do scenes-app-settings-identity-provider-configuration--oidc--light: @@ -12251,65 +12263,65 @@ snapshots: scenes-app-settings-identity-provider-configuration--xaa-needs-upgrade--light: hash: v1.k794b7964.9d93b0682bf14e191da3fa9614a1500ec01fbe7a9c313680c5fa2d776ae65806.KFggaDLcSJvceGa1JimS0m5ayPo6m_OtoH7_tzeqR-8 scenes-app-settings-organization--settings-organization-authentication--dark: - hash: v1.k794b7964.10cf86a319b6dbbd26e15ac3f03e7674838366cb05de992451eb59eea14a0c82.hD1hx2SFUdFf5-gga6QI-DCPbnHU5mQf0QMi18gnubo + hash: v1.k794b7964.21f0e6e4e45714487e01ad7caea8355561dd7fc20f94d2c47bf9af6dfbd6e694.GTlocFA0OxXP-aph5S37TexFbpIL_asF8RwsVko9fgA scenes-app-settings-organization--settings-organization-authentication--light: - hash: v1.k794b7964.bd85953593ca2e38573d79f532b4730bc977c47b9571a7c7fd377a9787dabe34.3epCB5zztPrdiKPrw951ic-63wCFa9A3SR7BpH1RwtM + hash: v1.k794b7964.51af2c907383ec85329880f1789c149be38ccd8bc5cea102bc25a0a62ea04232.s3VIXFw-6gxM4jPQTbZtwl3NI3r1L-dgkTkuzB72bSA scenes-app-settings-organization--settings-organization-billing--dark: - hash: v1.k794b7964.9c2fc7dbe005a63e8bdc1c8d1886819cb871b71d95b33aaf3ffcca61e20b85d6.XSkl4gws3y4DuB1zn-jdWxQDKXm0tvLYpLl4n0L638A + hash: v1.k794b7964.0e2ec0c2fc1ef05a02c0e46bff83e579af24f1632c149e3dd9907ef0b2d7cebf.QyzwHnbI18RNg7dfnBys-S78tpGrj3UHYmAXXjWmMwg scenes-app-settings-organization--settings-organization-billing--light: - hash: v1.k794b7964.a9d674f34d4e8cba39f0e063953203a8cd3bf3ce17518bf4040853331e6e45b0.gp9LeGBHPhfsdYkzuA1hIHcAyCJUyMjvXIiUEU2fMWI + hash: v1.k794b7964.daa4b6fda1204424af33b68386dd2f411913d2bff879649f10bcd7d0ffd97012.0kAKUsbVm8BmKsfacp4GYKNOaPm4R-KTOHcE_-0IdhY scenes-app-settings-organization--settings-organization-cimd-verification-tokens--dark: - hash: v1.k794b7964.e7c01a84f757c03ccd70f707658fe902dbbd3bd0f0d3d632ab50ce52db81327c.I9cHh8I55qbP4Uw0wQR5W15vh6AFSlLC_I0J387iR2o + hash: v1.k794b7964.10fd3b721a5fbb0734820ae6d7fdadaaefa546a25f16149bbf6f3d1a07a91f33.9S8jiMFz3IfM7Dl5ywk03YSsJ4FV6YIKU6WIErw2pY0 scenes-app-settings-organization--settings-organization-cimd-verification-tokens--light: - hash: v1.k794b7964.f8e6e09fece0606bf88bc56a28df620ca35205115c82ed2ba745da9728a7e94e.LqKzj4PosHsLg8DKJv2g324FNhR1TfZ7qZgQHJ885m8 + hash: v1.k794b7964.2a3d400b73887ba808abb2537cdca61bb1e1a6df3c42b869734a08f8b9463e1f.CUwzpTfRtDP_U6SqgKuoJZxzTt9EloLrjoLoTutNKno scenes-app-settings-organization--settings-organization-danger-zone--dark: - hash: v1.k794b7964.53cae501b65425eeeb065d19691d5ac2ae6fe6ba46d22fd9beef6221f9a1e9e5.MVJgHhQL0pG8mTmBnvjeOA--OHI0IY4s0uJ1EY06a-M + hash: v1.k794b7964.94aab2dfce72c0fe25a9048987b82aa9b3ce4d9220666aa9df229ece04a9dd09.rjwrQjtc_he0_ISvFfUsowLwWzpLekTNVMdNOXEoGnU scenes-app-settings-organization--settings-organization-danger-zone--light: - hash: v1.k794b7964.d94207cbb5cb28e15e4aa2811cfaa432549f1f6e9676a33be4af6691bcd2c2a5.9sB3V8VlyYhOIyA9Vw3NAOjcEww_FON4oT-Y9z5h3X4 + hash: v1.k794b7964.c37cfc7025afb29e2a33f7fcfcc60a97562b1f20c377011addc266a9ced9e157.H4o_tTpXEl5P7K_e6JFCcXr8KzYRXANgzr9bcOV5_rI scenes-app-settings-organization--settings-organization-details--dark: - hash: v1.k794b7964.aa20bd662940eed2aa7684314d23b2b9d1da59c223d48d930694b26176214b0f.r1bU7Yf6o_vFCFW3IsKpM_4TuG0SXPAlKjXJmx7ixYI + hash: v1.k794b7964.2256d4b72f1459da4dd1f581565e31bc9c616591f38811d042885d53e40de30e._GaTL5fs_SlTLyKIIhPghCfUmnHhDZ2NjV-I6-BFqWw scenes-app-settings-organization--settings-organization-details--light: - hash: v1.k794b7964.cc797b5816e079594c687cec240e13d5d3411403e247720f6fcaf5f0f5d72318.FMc09Ob0_HMh5d-SMZEDeOX4on05IH5IMBt7Acnp1_o + hash: v1.k794b7964.9aadaa5d43169d85abd8a2695d36a848037a6212cd63480e0fec1d997d14c458.5MXyd3TVv4r6fvypKcofqUEcxTNiOIsqwVlhkw1ameQ scenes-app-settings-organization--settings-organization-members--dark: - hash: v1.k794b7964.948d0954a165c99ab171b301859c367a8f9f12d0223cb15c21ddcdf1422db5cd.IZd4QIb61gDvqT4SsyTTSBb_3RJSlbpHBCj8KY4na1A + hash: v1.k794b7964.6aef318a653669fea6b6add79658543ed258f547bb106c37596d5c70ec8d4178.8OtqYfyqUVf3UTj0SAk9NYA6ewz0Ddo5rSwx5G_dGWY scenes-app-settings-organization--settings-organization-members--light: - hash: v1.k794b7964.e07387a759dc6da65770169c4fe4cb98c1d1003008cf5f582a3216ee0a9bab30.Jr1pIlyjYxv803qMJJuhMHhv_uJPI_nlK1nYl0Oon_0 + hash: v1.k794b7964.6ad7ec139f4a2df23d2e9f6885b92d0b0a158ca6ddb53922b732fb81ff1b1e36.ATusDWVeMcqrhUOLXDf3Pxu1P7eZNMUK7eGcekp5tKk scenes-app-settings-organization--settings-organization-proxy--dark: - hash: v1.k794b7964.8a48611a334ba1c56f561e631370afd0e44b1df18fad355ab94e1a4532d0ce84.McboI3SIr3q_k8jj8rfF_84-37fuWVXeQ544jxc_vQM + hash: v1.k794b7964.15bec0b0e82a5f683cb3c340e677d88f34f11e2b853a9b0a8979fe827f601caa.yy2dp_t-YIsX-IMk-akbGvaqwblNTRj-Ur-cEX-6pxM scenes-app-settings-organization--settings-organization-proxy--light: - hash: v1.k794b7964.aba4c78a1165f794a106feacf42b4a76632d612c6f40f29b73876a0d5287c0d0.vYTqNBN2Tt3VWCswaOjFndRsCtF4RsA_z0mUrGVo4X4 + hash: v1.k794b7964.836341561be871da316800d3cd36ce3c98d2a35cbc1d66bfd8c8372d0c62fd40.mTiMMQiDukmrEnqBmYjnn5_E5RhIX6-cSThsZ17M99I scenes-app-settings-organization--settings-organization-roles--dark: - hash: v1.k794b7964.d35d0644407ff52a6b5f78c40f0eb2f3b790a55617f0b9e0c06830108fd4e476.gD1y_gtWUf-N7hwNMlDyH1qm3YlTEc58cGc-xs4dkI8 + hash: v1.k794b7964.3751383b220c75d0d059dca99f9535e56cbe46efe2c701f148b0a4219ba020bf.C-Ycl1rZV0_MXecfEkR5t_BPQU1YTKrEl3-aKhJSSfs scenes-app-settings-organization--settings-organization-roles--light: - hash: v1.k794b7964.ad2b96d6dbf17c320ecf825fba78373f8e99cc19e76afb490214758f019532be.2qkeKvMmUNNUcpySEqt3Ek7cY4ZVKDHVWEt0gRHvC9o + hash: v1.k794b7964.486bde4f66e596418e980241eccd9e0516ddfb0fb26ad19c24e0c6a3833d0ade.Kw1_6pYcj5KQCSPBaUg9Y7-GCOczp9mIDJhj3i8AZRw scenes-app-settings-organization--settings-organization-startup-program--dark: - hash: v1.k794b7964.4270642327ef0e620b0fcf1c7293785fcd21b913ddc3c562c39f42982bf04465.HDv3_jxEB_Vj7dAGxTgVv-vNdde4gto0St17AFnZ0DM + hash: v1.k794b7964.ba55ab093e97ff8a0bff1134093465858176889a8ff3c3149223444c2ef65cbc.outhN0sFM9C9zHiO6wTEMF7N4bLqt-a9PqCyQqDF2Io scenes-app-settings-organization--settings-organization-startup-program--light: - hash: v1.k794b7964.9016cd7cbeec9dd377732c7e3067b77d6ea3f51a10adc322ffa0b9d11c2a2b05.LrClyfeQk_V83_vqiWZ6Q3SJPbZuW3VxKOuru2VAeEA + hash: v1.k794b7964.8b73e2f01ab2d61ffad479c1c86ef051d9e5161f10cb6d974ed04463171589cf.CV0uY4BKAdpB8EFeDYFTBhhW0b8COBr8i8u77Ohd24w scenes-app-settings-project--settings-project-access-control--dark: - hash: v1.k794b7964.c5ea10cc0b9014f139683d49a60329533bbcd774c281b8d58395797965583355.qRo6oX_MO91PTGyG1rt9vRhP4kDyts_nVSeWI6gw6VQ + hash: v1.k794b7964.bd575a88e736d757aa11e59d3323d2a6d93ea5ff7fca413650a5200aa3c446ff.guLxYVnwLG5G8LLRnRklJPt9akwBvuhkjdFnlEul8so scenes-app-settings-project--settings-project-access-control--light: - hash: v1.k794b7964.f349c742ec3ef646f88c4400f287569a4fd61d2f943010b37b7848daa49407d2.hpUfZ6m9YDEgYylfqQKLE_cGezkmn738em6fmwdYdmY + hash: v1.k794b7964.79c2e6f44840d3740d262d733ebd21ddaa6c12c80fd9af72002d83c5b8c46039.H8exb_6V2L6XPgohnW0YEbA5SzS_lojsyppVwCwhgho scenes-app-settings-project--settings-project-autocapture--dark: - hash: v1.k794b7964.e92fb6c59f611c46950a85444866f8aae03fe8c6713403fc0e1fa090a1d3471f.NS-EfkNFKqR9x53cuKBAK5Jwb3RRzFB0K44GMdJlgyA + hash: v1.k794b7964.367793a59557b95b8aa86c9e00c3bcc423034d459da8b28289a49b7bd15cea0e._ua3xjzAHeLUQIBGcuLqefAG46g4C-_dZem5Q_tstnE scenes-app-settings-project--settings-project-autocapture--light: - hash: v1.k794b7964.686906b13609b91551ae24ab45ae35b172b2bdaaf66eabd46d40fb4c44e4bde8.MANwMy04AxPk1gPftNkbi1DFc692rJ_c7mqQtZ5xrVA + hash: v1.k794b7964.6e179f57d058675781df8d0a2eb745eb63c758cfe4e022724844c924535facf2.XLMrhsGxohxpGCyNwbdWv-uxSpLcPxahE-UM1T2bIiU scenes-app-settings-project--settings-project-customization--dark: - hash: v1.k794b7964.ee2f6721a73ddb3d7039f049aae8875dd708a66f8216c3920cd5067daea467b9.CCLH8h_GmASECi0FE-VRxSlZ67Z1zkKKWJBmhHr9T0Q + hash: v1.k794b7964.71795ad88959169a3a4a17f6ce92757a2292766eed9ed3a3cdbaedc8affdadd0.Tjw6RIxtgMo9NVzSuVBagvAS2-O7k78k2ezjSs-hJPU scenes-app-settings-project--settings-project-customization--light: - hash: v1.k794b7964.6ef28ce703b233b976d1cfc18beaf438b4d42dbb2d24d2c74f230506d7dfc85e._oK61ibMjl2DJlkrM51CmcDur5M_OsKqeYji4pfswAw + hash: v1.k794b7964.a86c67fb8a8309bcf08260f5e5b15915800bb25e3fca5c23955a4f42565c7d39.3J_iXI1U2KhIC9NJLWEySt1rcjmyz-qdV6l_8uSSmVQ scenes-app-settings-project--settings-project-danger-zone--dark: - hash: v1.k794b7964.4be415dad77cd1906873786db55808942eeaecfbcf3b228c3c0d2151b888fa7c.1cEC7XnNveFuR6PC9jQd20m7PZZBU7l48v49CfgT8qU + hash: v1.k794b7964.0f6dc1b578f511c78066832f6c32a7bf1fcad55dba568e343cf7a6e2d71f8d57.dpRUSBG7w7RSVRdtomC2W5cXs571NoNtiThTJFNduQQ scenes-app-settings-project--settings-project-danger-zone--light: - hash: v1.k794b7964.b2fcdaef3a64a7d32e72c99bb2e7459f414332298eb9c107c18bb0f96fd4c57b.aUrhYzQEe_j7lZ83TOmJnrIrxfH-59VFZpehjiXhGnU + hash: v1.k794b7964.38d4dc36d43fef542fc1c49f9d1e6c8ab5f4b61b57781ee2a66177acb74afef8.3a_goMU_esAi-Fi6gLK8IHpkiWkPYhAyypaN2SzXXzE scenes-app-settings-project--settings-project-details--dark: - hash: v1.k794b7964.b4509f8083d8f1289e15997fd4a64b9b9719b001815d39a0a6ac2b623e0671b9.QDQ53WB-CKpjNUMmU0oxjoH2DeZgV_A4t3dGVXCnbog + hash: v1.k794b7964.8eb201b594afa8d78a6b75f9a6a08a11b3b114116e7dc28ecf5e262f0383da0a.CSOHqxFkKwDYwCMo0nCwJb3hvZnFG-sxmMRMMlQpphg scenes-app-settings-project--settings-project-details--light: - hash: v1.k794b7964.ee221714e08e9f75d8ee360c0a7a9ee26827772d50ae47e7cb6f13c4562a0783.bznURWK98FkoypEVCceYH123iP8F98TIdncaNXipOLk + hash: v1.k794b7964.049d345f0429bddbf2894d4dd40e34d3e288df344417ded61f6661748727ac61.WSSiL4Z_fjZ00_bx-hdI6G_WQiYI64Vohy4G0opZdvQ scenes-app-settings-project--settings-project-integrations--dark: - hash: v1.k794b7964.8bc9a0a3c68e9379000d45e767214553c220039f07512af99d61892720b7d9aa.kZh5Y-9-hiGOasm25v6q0g1yP19cwrxXfEJwFQViNgs + hash: v1.k794b7964.95cb5dadb923df180feb7d258e1d555847b13fdea54037e16ccd35ad87e1d17c.LT3vQw8uYN0USkoTFMe1KfAczu68RZ8BqnpYd0mcPFE scenes-app-settings-project--settings-project-integrations--light: - hash: v1.k794b7964.7e44eb5f34601ea71f789a14566d68ee8cd899515623d56eb7ac5de3b4a298c1.pFj4r7sp0_7x1jsWXx-KeXTNp5UGnrhntrc0Zg8TDqI + hash: v1.k794b7964.2ccb6b086646e33059ea4745778819c4ef81bbdee597a726c19fe7f0919547d2.ry_NWpiMbW2J4j3rgQ_jNnDZmc2I6oeOF7eI_Ir-H8o scenes-app-settings-project--settings-project-logs--dark: hash: v1.k794b7964.49d053d0783c37817aef5a402d8b00781837f6d4fcf96ce8018dcc439ee744f6.XDbj-HU8eX-2niM5nkM6KmkxOLY_sD30DE6_93XKRsU scenes-app-settings-project--settings-project-logs--light: @@ -12331,17 +12343,17 @@ snapshots: scenes-app-settings-project--settings-project-logs-read-only--light: hash: v1.k794b7964.4e11ad0c20b5ae6426397736a50c17ba21f6750d95d1a0a9f3106ed9cfc002ba.TrjQhBRaVixnEN_lFBvMriCqDdY0iA3Q2tdHRAybdvs scenes-app-settings-project--settings-project-product-analytics--dark: - hash: v1.k794b7964.b10de8b10188706f5c4f68dd47b9c025b3628ab5beb049146a0c720a88395d19.wmngiMzSh7nfJWDqAHtB9faWb82pf2wcY-iCh80QOlY + hash: v1.k794b7964.e57b7a6f926b5872a2b68f46582fd3e4e6b727596e3ef4a4558e779b49a352ac.5Me_sWEDBM3LMuxMe0H-k7LA-36g9C8MY4y2TQ4Vos4 scenes-app-settings-project--settings-project-product-analytics--light: - hash: v1.k794b7964.ecfb8d5b50fbb4178deeaaf082b9cd529c9609414250a745a1853e1e0c392824.whegFxqjAO0hFRuYhi0Kzwv_sxc8X97GQv4guMLnr2w + hash: v1.k794b7964.0cc6dd6b693d16d9f7631ff5b790c1119e2d0da257b35dbc7f34ddc1a5700bb4.-rrXfVpg290Lt-JqOFkg8lmkpwofu3dVyAwzwbYEcXA scenes-app-settings-project--settings-project-replay--dark: - hash: v1.k794b7964.31cac30c899ed647b34bb36014bbbda7d11127c3055104e29f7fb324e344ae6a.dacy0Z2WGO6lkTjBniWx5L9-kZlTvs3FT4Gck565yZ4 + hash: v1.k794b7964.8eb2299f4a7b8efa2d3e7455fced7b207586e109e3e2d4c817b999031dde2028.0beZpsyDcztzWHv21_Q2UHMIRHrg-ZWwP8n51mRbM8A scenes-app-settings-project--settings-project-replay--light: - hash: v1.k794b7964.e2a4fa6a1188d448757ab298da320c0116c3ca9b359512e649026320a2a78272.-p1Ozg4mB-LQInP-q-m_5I_lYVB_l_YHFA23OqFJ48w + hash: v1.k794b7964.4b85566b5497a9498c83ad146a76eb60a435fea1518e59b6d96c902aa612dda6.oFnC_qefDZb-EdLgXRdVv5nyaohCt-J1jVl7JjGggdg scenes-app-settings-project--settings-project-surveys--dark: - hash: v1.k794b7964.b2de0cd0533566c2406e9c66bcbe6a83c6f7b2bd0e1862027ba36a33a78624d1.D6LgyASNsXWSr98qx80h-brseJENrtIOMEJsSNN9sUo + hash: v1.k794b7964.93099f7e46eaa66e5b9f3eba6b70aa69ceaaf0786b95ed79a7753924d122b842.iC1qmybD8mAe7Ihi2ap5XtIqBFz9l9uv_966eEQFpwY scenes-app-settings-project--settings-project-surveys--light: - hash: v1.k794b7964.1dce2d50ae8e763257abefd34003f487df7baa41b6313291544ef850594c67eb.FZ2Kzj3Zzi8EPr4bXptpzNAps-OhQ0Je0kW17u3dXwc + hash: v1.k794b7964.18b3619d9e3e49addd3be0ea55bb6d742a10ff6bcf07db5cdbdf8d01f19311a2.BKdVuSg90tKnqBYoTRWiNFHeGA_qV17fhBNs1nTodSo scenes-app-settings-settingsectionunavailable--admin-only--dark: hash: v1.k794b7964.b6580b1a9c7735f0b03e9d5b58954169a1772b47bc2ae449f14771e1c5281a7b.Pp01b8BavwsYTSE4Mo3Q57hwN0X73qsh7JDbp7_DORo scenes-app-settings-settingsectionunavailable--admin-only--light: @@ -12351,9 +12363,9 @@ snapshots: scenes-app-settings-settingsectionunavailable--not-enabled--light: hash: v1.k794b7964.06e88a65c54124deb1ddd8236f4473695e5a87d5360ae5ab266779e9fc118f0b.m-LRG6zgQ_Ci4Y8_7H8N1OSIa2-GAzu9ZogXu0IDCdk scenes-app-settings-user--settings-user-api-keys--dark: - hash: v1.k794b7964.018cdff94d8593dea7f71a8beef867b5862dcccea79d7302ddd5bd2e7c22e84d.GANOBePxom88yyGcpQWwClTagqDPdu1QaundI0J0xuM + hash: v1.k794b7964.2d75083cdf2e66bd7fd1c49b50df5bb1f9fa8659b6ea163bf55c362ccd351369.tOJyeSQiK-0tZ-VbbuCPdjd9dkherBBtXbv5VpXRVfo scenes-app-settings-user--settings-user-api-keys--light: - hash: v1.k794b7964.9640239f3cb8279e7ccf48a26fe1079aacf98506f941db320ea4426766216507.SSXar-Jc8Nez5KO6KIBzJxxMOhj0dMrc1un4-jJrLD8 + hash: v1.k794b7964.03e1941c64420f230d05fea6c1ead519be74635f64d160ba806679b8b604cd72.pHshX3jwj9Hdgj1-IY0n5MrfPGK3boumqscizoQKsj0 scenes-app-settings-user--settings-user-backup-codes-modal--dark: hash: v1.k794b7964.fee174652504bedf45b5050dae77b7f38ffaac07d04c14c8c77b010d565cba97.n_drnTbGpOMa04Hy05-KZlJhx74bmdscz3V2bjDSQiw scenes-app-settings-user--settings-user-backup-codes-modal--light: @@ -12363,37 +12375,37 @@ snapshots: scenes-app-settings-user--settings-user-backup-codes-modal-generated--light: hash: v1.k794b7964.9c51bd1bd794659c4b6fdf9a5612dbf375e08f30375778c77cd79caff47c5aa7.s_2QKaTg7Wc2fBiUbAXjD4TG43A37T9Bpbrj3XFCN_Q scenes-app-settings-user--settings-user-customization--dark: - hash: v1.k794b7964.3045408e193e8e615527308d22dc413de653dda5d6c43805e7132e9f3840f4cb.b9QA2XgUqj92RL5nj3twT9YKKhCeCzTsVLAGPdQ6-ms + hash: v1.k794b7964.bfb0caa6e47a06900c62de982127308ab7a7cc4ebf648c041d708f4251169b97.W5kerKIGOMOu-nFXtXf-zSC7qru9QIZtkNae-6GMF5E scenes-app-settings-user--settings-user-customization--light: - hash: v1.k794b7964.060094abba224302a994668db661b12e8ae212196475ee499484f20f761b3d79.K4YyuPaBVOOQte4OrQrTtPBXeBhyknd6nyC6x4ztaIo + hash: v1.k794b7964.e72e96c0727036712edf311568f4ebc3ccfd4323b71459b4451a6bf9de4de402.7B5x64EE3syKdXKnKvrHKO3tfXIAcGm1sZPc9nQQCc8 scenes-app-settings-user--settings-user-danger-zone--dark: - hash: v1.k794b7964.4fa6c4238b06ef1c1dd0c749ef72a6e8d12e0a8d73f233bbf6ee15afe13f4c37.gqVHsXRhQu5FMrVIQCU_1b7zyTXlYVcJQAu3h4jJcFM + hash: v1.k794b7964.20666b0ba02fa802c917163c438121e25988703cc78e7b9b10498d1e84f29ffc.aikU_tsFRmKnGtJW8jfUO5WwcLGyjHC0ixu4tvqYdZk scenes-app-settings-user--settings-user-danger-zone--light: - hash: v1.k794b7964.588b1d5390399d7d71e55ed023f0af6576e5b37c8fa491b6d6ccbf123f48732b.M6r-NkphdCHOZ7CToPy_CTgIHFn4L1QP8YO2NLs-DW8 + hash: v1.k794b7964.3f66676bbf9ecd87e67bcc1082a651836f31073bbf2437b73f597f955088e4ec.3Dr-kNkWcF-z_3D1fJGIvhODgtwqGtO9nnaduH8Lwd4 scenes-app-settings-user--settings-user-navigation--dark: - hash: v1.k794b7964.8dd30d75f10b3f85822b3112314e290a388d0f4dbb6d1e9ba9a81fc28597cd48.sQAtho9f7Yt0I3JriLx4Ig_HTzp6pFZZ2foD9_3r8bY + hash: v1.k794b7964.7f97b6081336e9d7c05f27b11ab3c463f830430d64b86ef25aaca6065dea0070.hTIARsu-u16UlZlkFLS3zd9L4W1vqXrdcMcJvASyHr0 scenes-app-settings-user--settings-user-navigation--light: - hash: v1.k794b7964.ee8b6cb54e1187aa157a2bbeadd7fe04d4c5f1f25324a14d99e3db29cc57b451.sQdEM5dOgggHj23fURR3CQ9CM9-EZBHbFJiKoUpfczY + hash: v1.k794b7964.722ec73a88717e1d48ad24b519bfcd7fa5acc11ef148dfbd16f480f875453438.bUWYDrZzcB0-QpQtKuK4EMqtwp5ibiNNNmiYAK25Kv0 scenes-app-settings-user--settings-user-notifications--dark: - hash: v1.k794b7964.1fc4163c567f18057b929328e07459c13277805442523070136233c17d5e633a.Tbp1Kb6RmIhrzsmp7CWml5LEnFyy9iY9-A-lTK-h76c + hash: v1.k794b7964.a94b30c47d35141bca14de4c5b4dc72653f4124028d1a406a632616d754da53a.2Tdvbimb8y6SxBJTx7DNI1aC3CTzGRIPPJl4NdxAJ0A scenes-app-settings-user--settings-user-notifications--light: - hash: v1.k794b7964.1d238eb22fba4c223038a8a9be5d13ca4a7eb944f0ce07ed5402d270249923d9.HNm6rvMowebOKIqjSZORE_6aLQAHiqSd6DMw-Su8dYI + hash: v1.k794b7964.00f6c89b732eeb7798faeb348aa44f4965b8b987ad98d5cab507ad33b9d98eac.4k2dcG7-4JN2iJm8wbYd-PR3FtnbVXebrNX7wsT57E0 scenes-app-settings-user--settings-user-profile--dark: - hash: v1.k794b7964.83fafff8b1a57762a360843935973a8950f029750d5a4bf08f3190d7b3adfa27.bi_Feo8myzqGy96qOZRLMojF-VfkCWmC0qk5-ZJlGGc + hash: v1.k794b7964.dce2579b47f97e5a58d0a95ba629c3875b4abe191c065b60a33fa8687fb4776b.E9F1TcwoL6EA-w1BqMsxml5TjPif8IbFS4pYMaw7i7w scenes-app-settings-user--settings-user-profile--light: - hash: v1.k794b7964.a0b486f9b0e72b99cd0fd4f81e82cc03659f6e390ef250d2dc9ee962741a1a3a.S5gHNNqZtvyF_SXMgDISUTI6I85I72nc7v2OKtEIMZ0 + hash: v1.k794b7964.8ffe6b16f5e78062fd7c88a18a608a9331a1e36921f56883d5e8f1b664985b98.5gxdOTcOem6AzGIX1oHUW2ZwNQcqP_SJKOkeuA8X9Cc scenes-app-settings-user--settings-user-profile-hedgehog-avatar--dark: - hash: v1.k794b7964.20dfb98bd32dfc24e5970be5eec3ab3e7f7f2d2bf0d4a14ab7d5125ce8790af9.zJ9b3p2PBVjnlL9vtCssJJnh1KQtw8gDxm_LH2rKmy0 + hash: v1.k794b7964.3ee20208ed67fe5db40b58a20b2e1f6ac704dd9e375e2bed27de3ed02270b99c.kIAKvRKhyuk1BqK1Qyx5PNgZl4heWhfjwqlsszB9ZJA scenes-app-settings-user--settings-user-profile-hedgehog-avatar--light: - hash: v1.k794b7964.f084e072a7a26ec829a01c5ad48e6809cc6e3b74aeeb5f988832aa0e26bafa55.oQ-PmhBcXwy6i_GzZK_AkhqWwHq9DFgsNlrOi6hXp60 + hash: v1.k794b7964.71e76fb88d97b787ad91e975b2849e901a9ea4379208fd5f4d2f3097e8f29ba4.sfxcPbT_tgDW6GaSZ0Mdl6bKg2VDv_00B3BK9DuUW2k scenes-app-settings-user--settings-user-profile-stale-session--dark: - hash: v1.k794b7964.3b7c016ddd09e595239ead276cf9ee430ffc41ebbd80d611b21f5e8be484bcca.Ht4Lfz6Ty0dOyyhvXAHPE-gUW6Rvb8dAid1ZVaAh4sI + hash: v1.k794b7964.dce2579b47f97e5a58d0a95ba629c3875b4abe191c065b60a33fa8687fb4776b.TyitjPYUCQvHoaI2XPLKHiKLzAV0bSxwX4Onk-T4FD8 scenes-app-settings-user--settings-user-profile-stale-session--light: - hash: v1.k794b7964.7065458715c17c9b865c3574e941fe1ff3a9a8370f5774031d05e38033f9a919.0DsA-uuULQts_SJaYekwBuM3d5D0UcEh1o2adba-6J8 + hash: v1.k794b7964.8ffe6b16f5e78062fd7c88a18a608a9331a1e36921f56883d5e8f1b664985b98.NNTG12lYTC-5DWD6cDu8lOgd71p1vsDAzkEjlIELDw8 scenes-app-settings-user--settings-user-reminders-modal--dark: - hash: v1.k794b7964.d0767a8deef594f605dea9ffde6b7fc39276ba9dd2673558fbffa4452afa17a3.nAo3JBm4U0nqWPSI9hPJDAiTYAZjhytrSWTCcBeh5to + hash: v1.k794b7964.f474d12b308e62ba070e35b4d1e537ac76fd1a4b368a0b3c9d1fa523f2b5cee0.ucucXAC7boOiqiOJnQ4ScSHH3CHPK30MjZongFhmIHY scenes-app-settings-user--settings-user-reminders-modal--light: - hash: v1.k794b7964.fd4e6cef9717acd549a70ae2618b1327454c06028bab22e3f3f8bdebf37f94a7.Sy_iHcMFIQNixUcgJE0WkbH3pSmIwDeIRZoxT2EfEQU + hash: v1.k794b7964.6f628a820e5ed962eb5a7c47b69963b6a6510515d522770b007e73379c1794eb.ms6PW-3D-zwD-lK2XtJJaI3O0nk_4R8ya_TQhPeIVaU scenes-app-sidepanels--side-panel-access-control--dark: hash: v1.k794b7964.cde211ffacaa6094bdd1287c1518b614db47847807f8d0657fd2a17bed79bbc4.ZHgX_KskdNRu66iyKIFsK48AXBW2YgfbWKoR0QgJ_N4 scenes-app-sidepanels--side-panel-access-control--light: @@ -13423,17 +13435,17 @@ snapshots: scenes-other-onboarding-shared-wizard-sync-card--stalled--light: hash: v1.k794b7964.b52a107e85cd22687e6bc1dcf23a4067f185915e7aaa4518b39ef1bbfefe87ac.TU0cv3huC_bueumE3zh9JXnBoRGtM6FKvdbtqCcUL4c scenes-other-org-member-invites--current-user-is-admin--dark: - hash: v1.k794b7964.ec53183f80d7e83191a0e4dd75d07e2e8711744e44e6e0f8c0f0e418a34f7f06.NfjMFHuMD7wEXsV3BYObaYf4ZGHBFrYafbQOA7bh0O8 + hash: v1.k794b7964.be9a1ece2efdf9208c65d2a67a26292db96d2f60511fc10a6cb0d40d0fbc9c54.p0GvtSnXDf-lsCI5znv42HTMyE7ljSNXpFnuB59QN24 scenes-other-org-member-invites--current-user-is-admin--light: - hash: v1.k794b7964.b2d0b70d2e055ed6222eb4d07207554fa5a3c6c4c3f71a31bad36d175ff0d1b7.e-tehFiLj550rzzAnfHN_PYWaPaJ6xY-SoyTEsh13Ig + hash: v1.k794b7964.939816bff426d914c391b76c2c8bc1a1f96102f37917b27508c319d4e76c29c9.bIb_1hQ0FN2s_Rbe1pWhrQPOIDBAUtbfaIFvyMQ6yn4 scenes-other-org-member-invites--current-user-is-member--dark: - hash: v1.k794b7964.c7ed7d130d2baef7b4d264efd4690574a9c402f98506baa9ebfd5d518232e680.EdG48Pk4q8O9VthkPDVuidIQ74WojJ3d-dIsfxLDzJw + hash: v1.k794b7964.163babc85bd17648c957bdc14503b6206b9501ba3d716d8258726baa41d55c51.DgZPwclp8_xhfS9Js2Zep6H_nrpXVkysy4cVeNc7xxA scenes-other-org-member-invites--current-user-is-member--light: - hash: v1.k794b7964.798c856eb2da1354f0ad18fa67f9f1398ea7f371c0deeec3c374830c8c7768be.oeN5eo_NXtcHxgUVJ9_5TOV4-Amj2ImTSAKWT17TsQM + hash: v1.k794b7964.c5cf533ae1d47e7ec5f62bd46a66dc58c7fb88eea107631c54315e6b279c5b96.BnTpNMbz3u6u4HbHM-9UrH97Iz1JaVY9QK-xbGvjU30 scenes-other-org-member-invites--current-user-is-owner--dark: - hash: v1.k794b7964.ec53183f80d7e83191a0e4dd75d07e2e8711744e44e6e0f8c0f0e418a34f7f06.a3CB3mN1YwlYU9zbszIi_QLBM8SruNyLKd1oTwPH-oI + hash: v1.k794b7964.be9a1ece2efdf9208c65d2a67a26292db96d2f60511fc10a6cb0d40d0fbc9c54.rcSpTMZwZtdr1riSCvoqMNd8lNQcNHl0byhjV0afB60 scenes-other-org-member-invites--current-user-is-owner--light: - hash: v1.k794b7964.b2d0b70d2e055ed6222eb4d07207554fa5a3c6c4c3f71a31bad36d175ff0d1b7.O7gZlrxBwBy7o4DG5vAx-R4mku8UtyRPtWAJpM3kq54 + hash: v1.k794b7964.939816bff426d914c391b76c2c8bc1a1f96102f37917b27508c319d4e76c29c9.K5kylI3gzcqvqJklnKGZaQTFqZJEgj0Nwa6mKVLwcME scenes-other-organization-deactivated--deactivated--dark: hash: v1.k794b7964.c97f1ed943259f5567c27813c96bbb0566aca0adbf84459ee7d56b2ebea36b57.SgcQHuoYp4dLjnk797pwVsGonoLU6rH2XdLf0lrzefk scenes-other-organization-deactivated--deactivated--light: diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 6d418c25b80f..7bb5b106488b 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -454,6 +454,7 @@ export const FEATURE_FLAGS = { POSTHOG_AI_CONVERSATION_FEEDBACK_LLMA_SESSIONS: 'posthog-ai-conversation-feedback-llma-sessions', // owner: #team-posthog-ai POSTHOG_AI_QUEUE_MESSAGES_SYSTEM: 'posthog-ai-queue-messages-system', // owner: #team-posthog-ai POSTHOG_CODE_BILLING: 'posthog-code-billing', // owner: #team-desktop + POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD: 'posthog-code-codex-own-subscription-cloud', // owner: #team-desktop, gates the Codex subscription settings and the composer billing row POSTHOG_CODE_TASK_ANALYSIS: 'posthog-code-task-analysis', // owner: #team-desktop POSTHOG_CONNECT: 'posthog-connect', // owner: @Gilbert09, gates the "connect another PostHog project" personal integration POSTHOG_DESKTOP_CLOUD_COMPUTE_BILLING: 'posthog-desktop-cloud-compute-billing', // owner: #team-desktop diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 723e825d0264..c98de0ee144d 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -187,6 +187,7 @@ import { MCPHintsSetting } from './user/MCPHintsSetting' import { OptOutCapture } from './user/OptOutCapture' import { PasskeySettings } from './user/PasskeySettings' import { PersonalAPIKeys } from './user/PersonalAPIKeys' +import { PersonalCodexIntegration } from './user/PersonalCodexIntegration' import { PersonalGitHubIntegrations, PersonalSlackIntegrations } from './user/PersonalIntegrations' import { ProfilePictureSettings } from './user/ProfilePictureSettings' import { RealtimeNotificationPreferences } from './user/RealtimeNotificationPreferences' @@ -402,6 +403,23 @@ export const SETTINGS_MAP: SettingSection[] = [ }, ], }, + { + level: 'environment', + id: 'environment-ai-subscriptions', + title: 'Subscriptions', + group: 'AI', + flag: 'POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD', + settings: [ + { + id: 'ai-subscription-codex', + title: 'Codex', + description: + 'Connect your ChatGPT account so your Codex cloud tasks can run on your own ChatGPT plan. The connection is yours and applies in every project.', + component: , + keywords: ['codex', 'chatgpt', 'openai', 'subscription', 'plan', 'billing', 'cloud', 'personal'], + }, + ], + }, { level: 'environment', id: 'posthog-mcp', diff --git a/frontend/src/scenes/settings/stories/SettingsEnvironment.stories.tsx b/frontend/src/scenes/settings/stories/SettingsEnvironment.stories.tsx index db4e994804ae..953ec412fc80 100644 --- a/frontend/src/scenes/settings/stories/SettingsEnvironment.stories.tsx +++ b/frontend/src/scenes/settings/stories/SettingsEnvironment.stories.tsx @@ -1,6 +1,8 @@ import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' import type { Meta, StoryObj } from '@storybook/react' +import { within } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' import { useActions } from 'kea' import { router } from 'kea-router' import { useEffect, useState } from 'react' @@ -255,3 +257,55 @@ export const SettingsEnvironmentBusinessKnowledgeLearningOnSupportOff: Story = { }), ], } + +const codexSubscriptionMocks = (codex: Record): Record> => ({ + get: { + '/api/users/@me/integrations/codex/': codex, + }, +}) + +export const SettingsEnvironmentAiSubscriptionsCodexConnected: Story = { + args: { sectionId: 'environment-ai-subscriptions' }, + parameters: { + msw: { + mocks: codexSubscriptionMocks({ + status: 'connected', + plan_type: 'pro', + email: 'jane@example.com', + connected_at: '2023-05-20T10:00:00Z', + }), + }, + }, +} + +export const SettingsEnvironmentAiSubscriptionsCodexReauthRequired: Story = { + args: { sectionId: 'environment-ai-subscriptions' }, + parameters: { + msw: { + mocks: codexSubscriptionMocks({ + status: 'reauth_required', + plan_type: 'plus', + email: 'jane@example.com', + connected_at: '2023-05-20T10:00:00Z', + }), + }, + }, +} + +export const SettingsEnvironmentAiSubscriptionsCodexConnectModal: Story = { + args: { sectionId: 'environment-ai-subscriptions' }, + parameters: { + msw: { + mocks: codexSubscriptionMocks({ + status: 'not_connected', + plan_type: null, + email: null, + connected_at: null, + }), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await userEvent.click(await canvas.findByText('Connect Codex')) + }, +} diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index 5776b805a27a..750c6c0ed063 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -55,6 +55,7 @@ export type SettingSectionId = | 'environment-secret-api-keys' | 'environment-surveys' | 'environment-task-agents' + | 'environment-ai-subscriptions' | 'environment-tracing' | 'environment-web-analytics' | 'environment-workflows' @@ -289,6 +290,7 @@ export type SettingId = | 'surveys-default-appearance' | 'surveys-interface' | 'task-agent-my-preference' + | 'ai-subscription-codex' | 'task-agent-project-default' | 'theme' | 'tracing-distinct-id-attribute-keys' diff --git a/frontend/src/scenes/settings/user/CodexConnectModal.tsx b/frontend/src/scenes/settings/user/CodexConnectModal.tsx new file mode 100644 index 000000000000..ee8c01feb93e --- /dev/null +++ b/frontend/src/scenes/settings/user/CodexConnectModal.tsx @@ -0,0 +1,137 @@ +import { useActions, useValues } from 'kea' + +import { IconCheck } from '@posthog/icons' +import { LemonBanner, LemonButton, LemonInput, LemonModal, LemonSegmentedButton, Link } from '@posthog/lemon-ui' + +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { platformCommandControlKey } from 'lib/utils/dom' + +import { CODEX_LOGIN_PLATFORM_OPTIONS, codexLoginCommand } from './codexLoginCommands' +import { personalCodexIntegrationLogic } from './personalCodexIntegrationLogic' + +const PLATFORM_HINTS = { + macos: 'Run it in Terminal.', + linux: 'Run it in a terminal. It needs wl-clipboard, xclip, or xsel to copy to the clipboard.', + windows: 'Run it in PowerShell.', +} + +export const SETTINGS_CODEX_CONNECT_OPENER = 'settings' + +export interface CodexConnectModalProps { + /** Several surfaces can show the dialog at once. Each opens only for the opener that asked for it. */ + opener: string +} + +export function CodexConnectModal({ opener }: CodexConnectModalProps): JSX.Element { + const { connectModalOpener, authFileText, connectError, connecting, loginPlatform } = + useValues(personalCodexIntegrationLogic) + const { closeConnectModal, setLoginPlatform, commandCopied, pasteAuthFile, pasteFromClipboard, submitAuthFile } = + useActions(personalCodexIntegrationLogic) + const pasted = authFileText.length > 0 + + return ( + + + Cancel + + + Connect + + + } + > +
+
+
+

1. Run this command

+ +
+ + {codexLoginCommand(loginPlatform)} + +

+ {PLATFORM_HINTS[loginPlatform]} Open the link that it shows, sign in, and enter the + one-time code. The command copies your sign-in to the clipboard and deletes its temporary + folder, so nothing stays on your computer. +

+

+ You need the{' '} + + Codex CLI + + . Install it with npm install -g @openai/codex. +

+

+ Turn on device code login in your ChatGPT security settings first. In a ChatGPT workspace, an + admin turns it on in the workspace permissions. +

+
+
+

2. Paste the sign-in

+
+ undefined} + onPaste={(event) => { + event.preventDefault() + pasteAuthFile(event.clipboardData.getData('text')) + }} + placeholder={ + pasted ? 'Sign-in received' : `Click here and press ${platformCommandControlKey('V')}` + } + suffix={pasted ? : undefined} + autoComplete="off" + spellCheck={false} + disabled={connecting} + data-attr="codex-auth-file-input" + /> + + Paste from clipboard + +
+
+ {connectError ? {connectError} : null} + + PostHog never shows your sign-in and tries to clear your clipboard after you paste. It stores the + sign-in encrypted and uses it only for your Codex cloud tasks. You can disconnect at any time. + +
+
+ ) +} diff --git a/frontend/src/scenes/settings/user/PersonalCodexIntegration.tsx b/frontend/src/scenes/settings/user/PersonalCodexIntegration.tsx new file mode 100644 index 000000000000..895b80c5e16e --- /dev/null +++ b/frontend/src/scenes/settings/user/PersonalCodexIntegration.tsx @@ -0,0 +1,128 @@ +import { useActions, useValues } from 'kea' + +import { IconPlus, IconTerminal, IconTrash } from '@posthog/icons' +import { LemonBanner, LemonButton, LemonDialog, LemonSkeleton, LemonTag } from '@posthog/lemon-ui' + +import { TZLabel } from 'lib/components/TZLabel' + +import { CodexIntegrationStatusEnumApi, type UserCodexIntegrationApi } from '~/generated/core/api.schemas' + +import { CodexConnectModal, SETTINGS_CODEX_CONNECT_OPENER } from './CodexConnectModal' +import { personalCodexIntegrationLogic } from './personalCodexIntegrationLogic' + +function CodexAccountRow({ integration }: { integration: UserCodexIntegrationApi }): JSX.Element { + const { disconnectCodex } = useActions(personalCodexIntegrationLogic) + const { codexIntegrationLoading } = useValues(personalCodexIntegrationLogic) + + const handleDisconnect = (): void => { + LemonDialog.open({ + title: 'Disconnect Codex?', + description: ( +

+ PostHog removes your ChatGPT sign-in. Codex cloud tasks that use your ChatGPT plan stop working + until you connect again. +

+ ), + primaryButton: { + children: 'Disconnect', + status: 'danger', + onClick: () => disconnectCodex(), + }, + secondaryButton: { children: 'Cancel' }, + }) + } + + return ( +
+
+
+ +
+
+
+
+ {integration.email || 'ChatGPT account'} + {integration.plan_type ? ( + + {`ChatGPT ${integration.plan_type}`} + + ) : null} +
+
+ {integration.connected_at ? ( + <> + Connected + + + ) : ( + 'Connected' + )} +
+ {integration.status === CodexIntegrationStatusEnumApi.ReauthRequired ? ( + + Your ChatGPT sign-in stopped working. Connect Codex again to keep using your ChatGPT plan. + + ) : null} +
+
+ } + onClick={handleDisconnect} + disabledReason={codexIntegrationLoading ? 'Loading…' : undefined} + tooltip="Disconnect Codex" + data-attr="codex-disconnect" + /> +
+
+ ) +} + +export function PersonalCodexIntegration(): JSX.Element { + const { codexIntegration, codexIntegrationLoadFailed, connecting } = useValues(personalCodexIntegrationLogic) + const { openConnectModal } = useActions(personalCodexIntegrationLogic) + + if (codexIntegration === null) { + return codexIntegrationLoadFailed ? ( + Could not load your Codex connection. Refresh the page to try again. + ) : ( + + ) + } + + const notConnected = codexIntegration.status === CodexIntegrationStatusEnumApi.NotConnected + const reauthRequired = codexIntegration.status === CodexIntegrationStatusEnumApi.ReauthRequired + + return ( + <> +
+ {notConnected ? ( +
+ +

No Codex account connected

+
+ ) : ( + + )} + {notConnected || reauthRequired ? ( +
+ } + onClick={() => openConnectModal(SETTINGS_CODEX_CONNECT_OPENER)} + disabledReason={connecting ? 'Connecting…' : undefined} + data-attr="codex-connect" + > + {reauthRequired ? 'Connect again' : 'Connect Codex'} + + You need a paid ChatGPT plan. +
+ ) : null} +
+ + + ) +} diff --git a/frontend/src/scenes/settings/user/codexAuthFile.test.ts b/frontend/src/scenes/settings/user/codexAuthFile.test.ts new file mode 100644 index 000000000000..72a0a83b8382 --- /dev/null +++ b/frontend/src/scenes/settings/user/codexAuthFile.test.ts @@ -0,0 +1,37 @@ +import { parseCodexAuthFile } from './codexAuthFile' + +describe('parseCodexAuthFile', () => { + it('reads the tokens from a ChatGPT auth.json', () => { + const text = JSON.stringify({ + OPENAI_API_KEY: null, + tokens: { + id_token: 'fake-id-token', + access_token: 'fake-access-token', + refresh_token: 'fake-refresh-token', + account_id: 'fake-account', + }, + last_refresh: '2026-01-01T00:00:00Z', + }) + + expect(parseCodexAuthFile(`\n${text}\n`)).toEqual({ + tokens: { + access_token: 'fake-access-token', + refresh_token: 'fake-refresh-token', + id_token: 'fake-id-token', + }, + error: null, + }) + }) + + it.each([ + ['text that is not JSON', 'codex login', 'does not hold a Codex sign-in'], + ['an API key login', JSON.stringify({ OPENAI_API_KEY: 'sk-fake', tokens: null }), 'not with an API key'], + ['a JSON array', '[]', 'not with an API key'], + ['a file without a refresh token', JSON.stringify({ tokens: { access_token: 'fake' } }), 'missing its tokens'], + ])('rejects %s', (_name, text, expectedError) => { + const result = parseCodexAuthFile(text) + + expect(result.tokens).toBeNull() + expect(result.error).toContain(expectedError) + }) +}) diff --git a/frontend/src/scenes/settings/user/codexAuthFile.ts b/frontend/src/scenes/settings/user/codexAuthFile.ts new file mode 100644 index 000000000000..265118facbeb --- /dev/null +++ b/frontend/src/scenes/settings/user/codexAuthFile.ts @@ -0,0 +1,36 @@ +import type { UserCodexAuthTokensApi } from '~/generated/core/api.schemas' + +export type CodexAuthFileResult = { tokens: UserCodexAuthTokensApi; error: null } | { tokens: null; error: string } + +function readString(value: unknown): string | null { + return typeof value === 'string' && value.trim() ? value.trim() : null +} + +export function parseCodexAuthFile(text: string): CodexAuthFileResult { + let parsed: unknown + try { + parsed = JSON.parse(text) + } catch { + return { + tokens: null, + error: 'The clipboard does not hold a Codex sign-in. Run the command again, then paste.', + } + } + const rawTokens = parsed && typeof parsed === 'object' ? (parsed as Record).tokens : undefined + if (!rawTokens || typeof rawTokens !== 'object') { + return { + tokens: null, + error: 'This is not a ChatGPT sign-in. Run the command again and sign in with ChatGPT, not with an API key.', + } + } + const tokens = rawTokens as Record + const accessToken = readString(tokens.access_token) + const refreshToken = readString(tokens.refresh_token) + if (!accessToken || !refreshToken) { + return { tokens: null, error: 'This sign-in is missing its tokens. Run the command again.' } + } + return { + tokens: { access_token: accessToken, refresh_token: refreshToken, id_token: readString(tokens.id_token) }, + error: null, + } +} diff --git a/frontend/src/scenes/settings/user/codexLoginCommands.test.ts b/frontend/src/scenes/settings/user/codexLoginCommands.test.ts new file mode 100644 index 000000000000..2644dc8d32ca --- /dev/null +++ b/frontend/src/scenes/settings/user/codexLoginCommands.test.ts @@ -0,0 +1,15 @@ +import { detectCodexLoginPlatform } from './codexLoginCommands' + +describe('detectCodexLoginPlatform', () => { + it.each([ + ['macOS', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', 'macos'], + ['MacIntel', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Version/17.0 Safari/605.1.15', 'macos'], + ['Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'windows'], + ['Win32', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', 'windows'], + ['Linux x86_64', 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0', 'linux'], + ['Chrome OS', 'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36', 'linux'], + ['', '', 'macos'], + ])('maps platform %j to the right command', (platform, userAgent, expected) => { + expect(detectCodexLoginPlatform(platform, userAgent)).toBe(expected) + }) +}) diff --git a/frontend/src/scenes/settings/user/codexLoginCommands.ts b/frontend/src/scenes/settings/user/codexLoginCommands.ts new file mode 100644 index 000000000000..edf9c9c1f520 --- /dev/null +++ b/frontend/src/scenes/settings/user/codexLoginCommands.ts @@ -0,0 +1,59 @@ +export type CodexLoginPlatform = 'macos' | 'linux' | 'windows' + +export const CODEX_LOGIN_PLATFORM_OPTIONS: { value: CodexLoginPlatform; label: string }[] = [ + { value: 'macos', label: 'macOS' }, + { value: 'linux', label: 'Linux' }, + { value: 'windows', label: 'Windows' }, +] + +const CODEX_LOGIN = 'codex login --device-auth -c cli_auth_credentials_store=file' +const COPIED_MESSAGE = 'Copied. Paste it in PostHog.' + +const LINUX_COPY = + 'if [ -n "$WAYLAND_DISPLAY" ] && command -v wl-copy >/dev/null; then wl-copy < "$d/auth.json"; ' + + 'elif command -v xclip >/dev/null; then xclip -selection clipboard < "$d/auth.json"; ' + + 'else xsel --clipboard --input < "$d/auth.json"; fi' + +function posixLoginCommand(copyToClipboard: string): string { + return ( + `sh -c 'd=$(mktemp -d) && trap "rm -rf \\"$d\\"" EXIT INT TERM HUP && ` + + `CODEX_HOME="$d" ${CODEX_LOGIN} && ${copyToClipboard} && echo "${COPIED_MESSAGE}"'` + ) +} + +const WINDOWS_LOGIN_COMMAND = + '$d = Join-Path $env:TEMP ([guid]::NewGuid()); $h = $env:CODEX_HOME; ' + + 'New-Item -ItemType Directory -Path $d | Out-Null; ' + + `try { $env:CODEX_HOME = $d; ${CODEX_LOGIN}; ` + + `if ($LASTEXITCODE -eq 0) { Get-Content -Raw (Join-Path $d 'auth.json') | Set-Clipboard; '${COPIED_MESSAGE}' } } ` + + 'finally { $env:CODEX_HOME = $h; Remove-Item -Recurse -Force $d }' + +export function codexLoginCommand(platform: CodexLoginPlatform): string { + switch (platform) { + case 'macos': + return posixLoginCommand('pbcopy < "$d/auth.json"') + case 'linux': + return posixLoginCommand(LINUX_COPY) + case 'windows': + return WINDOWS_LOGIN_COMMAND + } +} + +export function detectCodexLoginPlatform(platform: string, userAgent: string): CodexLoginPlatform { + const source = `${platform} ${userAgent}`.toLowerCase() + if (source.includes('mac')) { + return 'macos' + } + if (source.includes('win')) { + return 'windows' + } + if (source.includes('linux') || source.includes('x11') || source.includes('cros')) { + return 'linux' + } + return 'macos' +} + +export function browserCodexLoginPlatform(): CodexLoginPlatform { + const nav = navigator as Navigator & { userAgentData?: { platform?: string } } + return detectCodexLoginPlatform(nav.userAgentData?.platform || nav.platform || '', nav.userAgent) +} diff --git a/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.test.ts b/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.test.ts new file mode 100644 index 000000000000..8465c3003d53 --- /dev/null +++ b/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.test.ts @@ -0,0 +1,111 @@ +import { expectLogic } from 'kea-test-utils' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + +import { personalCodexIntegrationLogic } from './personalCodexIntegrationLogic' + +const AUTH_FILE = JSON.stringify({ + tokens: { access_token: 'fake-access', refresh_token: 'fake-refresh', id_token: 'fake-id' }, +}) +const CONNECTED = { status: 'connected', email: 'jane@example.com', plan_type: 'pro' } +const NOT_CONNECTED = { status: 'not_connected' } + +function deferred(): { promise: Promise; resolve: (value: T) => void } { + let resolve!: (value: T) => void + const promise = new Promise((res) => { + resolve = res + }) + return { promise, resolve } +} + +describe('personalCodexIntegrationLogic', () => { + let logic: ReturnType + let connectRequests: number + let pendingRead: ReturnType> | null + + beforeEach(() => { + connectRequests = 0 + pendingRead = null + useMocks({ + get: { + '/api/users/@me/integrations/codex/': async () => { + await pendingRead?.promise + return [200, NOT_CONNECTED] + }, + }, + post: { + '/api/users/@me/integrations/codex/': () => { + connectRequests += 1 + return [200, CONNECTED] + }, + }, + }) + initKeaTests() + }) + + afterEach(() => { + logic?.unmount() + }) + + it('does not connect when the dialog closes before the clipboard read finishes', async () => { + const clipboard = deferred() + Object.assign(navigator, { clipboard: { readText: () => clipboard.promise, writeText: jest.fn() } }) + logic = personalCodexIntegrationLogic() + logic.mount() + await expectLogic(logic).toFinishAllListeners() + + logic.actions.openConnectModal('settings') + logic.actions.pasteFromClipboard() + logic.actions.closeConnectModal() + clipboard.resolve(AUTH_FILE) + await expectLogic(logic).toFinishAllListeners() + + expect(connectRequests).toBe(0) + expect(logic.values.authFileText).toBe('') + }) + + it('connects once when two clipboard reads land together', async () => { + const clipboard = deferred() + Object.assign(navigator, { clipboard: { readText: () => clipboard.promise, writeText: jest.fn() } }) + logic = personalCodexIntegrationLogic() + logic.mount() + await expectLogic(logic).toFinishAllListeners() + + logic.actions.openConnectModal('settings') + logic.actions.pasteFromClipboard() + logic.actions.pasteFromClipboard() + clipboard.resolve(AUTH_FILE) + await expectLogic(logic).toDispatchActions(['connectCodexSuccess']).toFinishAllListeners() + + expect(connectRequests).toBe(1) + }) + + it('drops an invalid paste so the dialog does not show it as received', async () => { + logic = personalCodexIntegrationLogic() + logic.mount() + logic.actions.openConnectModal('settings') + + logic.actions.pasteAuthFile('codex login --device-auth') + await expectLogic(logic).toFinishAllListeners() + + expect(logic.values.authFileText).toBe('') + expect(logic.values.connectError).toContain('does not hold a Codex sign-in') + expect(connectRequests).toBe(0) + }) + + it('keeps a new connection when an older reload finishes after it', async () => { + pendingRead = deferred() + logic = personalCodexIntegrationLogic() + logic.mount() + + logic.actions.openConnectModal('settings') + logic.actions.pasteAuthFile(AUTH_FILE) + await expectLogic(logic, () => undefined).toDispatchActions(['connectCodexSuccess']) + pendingRead.resolve() + await expectLogic(logic).toFinishAllListeners() + + expect(connectRequests).toBe(1) + expect(logic.values.codexIntegration?.status).toBe('connected') + }) +}) diff --git a/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.ts b/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.ts new file mode 100644 index 000000000000..e82a908e46a1 --- /dev/null +++ b/frontend/src/scenes/settings/user/personalCodexIntegrationLogic.ts @@ -0,0 +1,302 @@ +import { MakeLogicType, actions, events, kea, listeners, path, reducers } from 'kea' +import { loaders } from 'kea-loaders' +import posthog from 'posthog-js' + +import { lemonToast } from '@posthog/lemon-ui' + +import { + usersIntegrationsCodexCreate, + usersIntegrationsCodexDestroy, + usersIntegrationsCodexRetrieve, +} from '~/generated/core/api' +import { + CodexIntegrationStatusEnumApi, + type UserCodexAuthTokensApi, + type UserCodexIntegrationApi, +} from '~/generated/core/api.schemas' + +import { parseCodexAuthFile } from './codexAuthFile' +import { type CodexLoginPlatform, browserCodexLoginPlatform } from './codexLoginCommands' + +const CONNECT_ERROR_FALLBACK = 'Could not connect Codex. Run the command again, then paste the new sign-in.' +const CLIPBOARD_BLOCKED_ERROR = 'Your browser blocked clipboard access. Click the field and paste with your keyboard.' + +async function clearClipboard(): Promise { + try { + await navigator.clipboard.writeText('') + } catch { + lemonToast.warning('Could not clear your clipboard. Copy something else to remove the sign-in from it.') + } +} + +function describeConnectError(error: unknown): string { + const detail = error && typeof error === 'object' ? (error as { detail?: unknown }).detail : undefined + return typeof detail === 'string' && detail ? detail : CONNECT_ERROR_FALLBACK +} + +// Generated by kea-typegen. Update if you're an agent, ignore if you're human. +export interface personalCodexIntegrationLogicValues { + authFileText: string + codexIntegration: UserCodexIntegrationApi | null + codexIntegrationLoadFailed: boolean + codexIntegrationLoading: boolean + codexMutationCount: number + connectError: string | null + connectModalOpener: string | null + connectModalSession: number + connecting: boolean + loginPlatform: CodexLoginPlatform +} + +// Generated by kea-typegen. Update if you're an agent, ignore if you're human. +export interface personalCodexIntegrationLogicActions { + closeConnectModal: () => { + value: true + } + commandCopied: () => { + value: true + } + connectCodex: (tokens: UserCodexAuthTokensApi) => UserCodexAuthTokensApi + connectCodexFailure: ( + error: string, + errorObject?: any + ) => { + error: string + errorObject?: any + } + connectCodexSuccess: ( + codexIntegration: UserCodexIntegrationApi, + payload?: UserCodexAuthTokensApi + ) => { + codexIntegration: UserCodexIntegrationApi + payload?: UserCodexAuthTokensApi + } + disconnectCodex: () => any + disconnectCodexFailure: ( + error: string, + errorObject?: any + ) => { + error: string + errorObject?: any + } + disconnectCodexSuccess: ( + codexIntegration: UserCodexIntegrationApi, + payload?: any + ) => { + codexIntegration: UserCodexIntegrationApi + payload?: any + } + loadCodexIntegration: () => any + loadCodexIntegrationFailure: ( + error: string, + errorObject?: any + ) => { + error: string + errorObject?: any + } + loadCodexIntegrationSuccess: ( + codexIntegration: UserCodexIntegrationApi | null, + payload?: any + ) => { + codexIntegration: UserCodexIntegrationApi | null + payload?: any + } + openConnectModal: (opener: string) => { + opener: string + } + pasteAuthFile: (authFileText: string) => { + authFileText: string + } + pasteFromClipboard: () => { + value: true + } + rejectAuthFile: (error: string) => { + error: string + } + setConnectError: (connectError: string | null) => { + connectError: string | null + } + setLoginPlatform: (loginPlatform: CodexLoginPlatform) => { + loginPlatform: CodexLoginPlatform + } + submitAuthFile: () => { + value: true + } +} + +export type personalCodexIntegrationLogicType = MakeLogicType< + personalCodexIntegrationLogicValues, + personalCodexIntegrationLogicActions +> + +export const personalCodexIntegrationLogic = kea([ + path(['scenes', 'settings', 'user', 'personalCodexIntegrationLogic']), + + actions({ + openConnectModal: (opener: string) => ({ opener }), + closeConnectModal: true, + setLoginPlatform: (loginPlatform: CodexLoginPlatform) => ({ loginPlatform }), + commandCopied: true, + pasteAuthFile: (authFileText: string) => ({ authFileText }), + pasteFromClipboard: true, + setConnectError: (connectError: string | null) => ({ connectError }), + submitAuthFile: true, + rejectAuthFile: (error: string) => ({ error }), + }), + + loaders(({ values }) => ({ + codexIntegration: [ + null as UserCodexIntegrationApi | null, + { + loadCodexIntegration: async () => { + const mutationCount = values.codexMutationCount + const integration = await usersIntegrationsCodexRetrieve('@me') + // A connect or disconnect that started meanwhile is newer than this read. + return values.codexMutationCount === mutationCount ? integration : values.codexIntegration + }, + connectCodex: async (tokens: UserCodexAuthTokensApi) => + await usersIntegrationsCodexCreate('@me', { tokens }), + disconnectCodex: async (): Promise => { + await usersIntegrationsCodexDestroy('@me') + return { status: CodexIntegrationStatusEnumApi.NotConnected } + }, + }, + ], + })), + + reducers({ + codexMutationCount: [ + 0, + { + connectCodex: (count) => count + 1, + disconnectCodex: (count) => count + 1, + }, + ], + connectModalSession: [ + 0, + { + openConnectModal: (session) => session + 1, + closeConnectModal: (session) => session + 1, + connectCodexSuccess: (session) => session + 1, + }, + ], + connectModalOpener: [ + null as string | null, + { + openConnectModal: (_, { opener }) => opener, + closeConnectModal: () => null, + connectCodexSuccess: () => null, + }, + ], + loginPlatform: [ + browserCodexLoginPlatform(), + { + setLoginPlatform: (_, { loginPlatform }) => loginPlatform, + }, + ], + authFileText: [ + '', + { + pasteAuthFile: (_, { authFileText }) => authFileText, + rejectAuthFile: () => '', + openConnectModal: () => '', + closeConnectModal: () => '', + connectCodexSuccess: () => '', + }, + ], + connectError: [ + null as string | null, + { + setConnectError: (_, { connectError }) => connectError, + rejectAuthFile: (_, { error }) => error, + pasteAuthFile: () => null, + openConnectModal: () => null, + }, + ], + connecting: [ + false, + { + connectCodex: () => true, + connectCodexSuccess: () => false, + connectCodexFailure: () => false, + }, + ], + codexIntegrationLoadFailed: [ + false, + { + loadCodexIntegrationFailure: () => true, + loadCodexIntegrationSuccess: () => false, + }, + ], + }), + + listeners(({ actions, values }) => ({ + openConnectModal: ({ opener }) => { + posthog.capture('personal integration connect clicked', { + integration_kind: 'codex', + surface: opener.split(':')[0], + }) + }, + commandCopied: () => { + posthog.capture('codex connect command copied', { operating_system: values.loginPlatform }) + }, + pasteAuthFile: () => { + actions.submitAuthFile() + }, + pasteFromClipboard: async () => { + const session = values.connectModalSession + let text: string + try { + text = await navigator.clipboard.readText() + } catch { + actions.setConnectError(CLIPBOARD_BLOCKED_ERROR) + return + } + // The read can outlive the dialog it came from. A closed or reopened dialog must not connect. + if (values.connectModalSession === session && values.connectModalOpener !== null) { + actions.pasteAuthFile(text) + } + }, + submitAuthFile: () => { + // A second clipboard read can land while the first connect runs. Its refresh token is single-use. + if (values.connecting) { + return + } + const result = parseCodexAuthFile(values.authFileText) + if (result.tokens === null) { + posthog.capture('codex subscription connect failed', { reason: 'invalid_auth_file' }) + actions.rejectAuthFile(result.error) + return + } + void clearClipboard() + actions.connectCodex(result.tokens) + }, + connectCodexSuccess: () => { + lemonToast.success('Codex connected') + }, + connectCodexFailure: ({ errorObject }) => { + actions.setConnectError(describeConnectError(errorObject)) + }, + disconnectCodexSuccess: () => { + lemonToast.success('Codex disconnected') + }, + disconnectCodexFailure: () => { + lemonToast.error('Could not disconnect Codex. Try again.') + }, + })), + + events(({ actions, cache }) => ({ + afterMount: () => { + actions.loadCodexIntegration() + cache.disposables.add( + () => { + const onFocus = (): void => actions.loadCodexIntegration() + window.addEventListener('focus', onFocus) + return () => window.removeEventListener('focus', onFocus) + }, + 'focusRefetch', + { pauseOnPageHidden: false } + ) + }, + })), +]) diff --git a/products/posthog_ai/frontend/components/composer/ComposerCodexBillingPickers.tsx b/products/posthog_ai/frontend/components/composer/ComposerCodexBillingPickers.tsx new file mode 100644 index 000000000000..c21036218fc5 --- /dev/null +++ b/products/posthog_ai/frontend/components/composer/ComposerCodexBillingPickers.tsx @@ -0,0 +1,48 @@ +import { useActions, useValues } from 'kea' +import { useId } from 'react' + +import { CodexConnectModal } from 'scenes/settings/user/CodexConnectModal' + +import { ModelAccessEnumApi } from 'products/tasks/frontend/generated/api.schemas' + +import { codexBillingLogic } from '../../logics/codexBillingLogic' +import { ComposerModelEffortPickers, type ComposerModelEffortPickersProps } from './ComposerModelEffortPickers' + +export interface ComposerCodexBillingPickersProps extends Omit { + /** The billing a live run booted with. A live run can't change it, so the row shows it locked. */ + lockedCodexModelAccess?: string | null +} + +/** + * The model picker plus the Codex billing row. Rendered only behind the rollout flag, so users without the + * flag never load their ChatGPT connection. + */ +export function ComposerCodexBillingPickers({ + lockedCodexModelAccess, + ...pickerProps +}: ComposerCodexBillingPickersProps): JSX.Element { + const { effectiveCodexModelAccess, planConnected } = useValues(codexBillingLogic) + const { setPreferredCodexModelAccess, connectPlan } = useActions(codexBillingLogic) + const opener = `composer:${useId()}` + const locked = lockedCodexModelAccess != null + + return ( + <> + connectPlan(opener), + }} + /> + + + ) +} diff --git a/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.test.tsx b/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.test.tsx index 2ed81d1b98e1..1373698084e1 100644 --- a/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.test.tsx +++ b/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.test.tsx @@ -3,6 +3,7 @@ import '@testing-library/jest-dom' import { type RenderResult, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { + ModelAccessEnumApi, ModelChoiceApi, ReasoningEffortEnumApi, RuntimeAdapterEnumApi, @@ -80,6 +81,62 @@ describe('ComposerModelEffortPickers', () => { expect(onModelChange).toHaveBeenCalledWith(expectedModel) }) + describe('Codex billing', () => { + const withCodex: ModelChoiceApi[] = [ + ...CATALOGUE, + { + runtime_adapter: RuntimeAdapterEnumApi.Codex, + model: 'gpt-6-sol', + display_name: 'GPT-6 Sol', + supported_efforts: [ReasoningEffortEnumApi.High], + }, + ] + const billing = (overrides = {}): React.ComponentProps['codexBilling'] => ({ + value: ModelAccessEnumApi.PosthogGateway, + planConnected: false, + onChange: jest.fn(), + onConnectPlan: jest.fn(), + ...overrides, + }) + + it('is not offered on the Claude harness', () => { + renderPickers({ models: withCodex, codexBilling: billing() }) + + expect(screen.getByText('Harness')).toBeInTheDocument() + expect(screen.queryByText('Billing')).not.toBeInTheDocument() + }) + + it('keeps the ChatGPT plan out of reach until an account is connected', async () => { + const onConnectPlan = jest.fn() + renderPickers({ + models: withCodex, + selectedModel: 'gpt-6-sol', + codexBilling: billing({ onConnectPlan }), + }) + fireEvent.click(screen.getByText('Advanced')) + fireEvent.click(screen.getByText('Billing')) + + expect(await screen.findByText('OpenAI (ChatGPT plan)')).toHaveAttribute('aria-disabled', 'true') + fireEvent.click(screen.getByText('Connect your ChatGPT account')) + await waitFor(() => expect(onConnectPlan).toHaveBeenCalledTimes(1)) + }) + + it('switches a Codex run to the ChatGPT plan once connected', async () => { + const onChange = jest.fn() + renderPickers({ + models: withCodex, + selectedModel: 'gpt-6-sol', + codexBilling: billing({ planConnected: true, onChange }), + }) + fireEvent.click(screen.getByText('Advanced')) + fireEvent.click(screen.getByText('Billing')) + fireEvent.click(await screen.findByText('OpenAI (ChatGPT plan)')) + + expect(onChange).toHaveBeenCalledWith(ModelAccessEnumApi.OwnSubscription) + expect(screen.queryByText('Connect your ChatGPT account')).not.toBeInTheDocument() + }) + }) + it('offers no way to change the default on a surface that has none to change', () => { renderPickers() diff --git a/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.tsx b/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.tsx index 70d2986856bd..8fb89d1092c6 100644 --- a/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.tsx +++ b/products/posthog_ai/frontend/components/composer/ComposerModelEffortPickers.tsx @@ -28,6 +28,7 @@ import { modelsForRuntimeAdapter, } from 'products/posthog_ai/frontend/utils/composerModels' import { + ModelAccessEnumApi, ModelChoiceApi, ReasoningEffortEnumApi, RuntimeAdapterEnumApi, @@ -40,6 +41,21 @@ import { ComposerReasoningSlider } from './ComposerReasoningSlider' // Separates model and effort in a slider stop key; never appears in a model id or an effort. const STOP_SEPARATOR = '|' +const BILLING_LABELS: Record = { + [ModelAccessEnumApi.PosthogGateway]: 'PostHog credits', + [ModelAccessEnumApi.OwnSubscription]: 'OpenAI (ChatGPT plan)', +} + +export interface ComposerCodexBilling { + value: ModelAccessEnumApi + /** The ChatGPT plan can only be picked once the user connected a ChatGPT account. */ + planConnected: boolean + /** A live run keeps the billing it booted with. */ + locked?: boolean + onChange: (value: ModelAccessEnumApi) => void + onConnectPlan: () => void +} + export interface ComposerModelEffortPickersProps { /** Models to offer, and the efforts each supports. Callers pass `modelCatalogueLogic`'s live catalogue. */ models: ModelChoiceApi[] @@ -65,6 +81,8 @@ export interface ComposerModelEffortPickersProps { * the picker stays free of the app's routing, and an embedding host can send its own audience * somewhere else. Omit and the row is absent. */ onOpenDefaultSettings?: () => void + /** Who pays for a run on the Codex harness. Shown only while Codex is selected; omit to hide the row. */ + codexBilling?: ComposerCodexBilling } interface PickerSectionProps { @@ -118,6 +136,7 @@ export function ComposerModelEffortPickers({ isDefaultSelection = false, onResetToDefault, onOpenDefaultSettings, + codexBilling, }: ComposerModelEffortPickersProps): JSX.Element { const [open, setOpen] = useState(false) const [advanced, setAdvanced] = useState(false) @@ -179,6 +198,8 @@ export function ComposerModelEffortPickers({ } } + const billing = selectedAdapter === RuntimeAdapterEnumApi.Codex ? codexBilling : undefined + // With neither a configured default to fall back to nor a ladder to land on, there is nothing to reset to. const showReset = Boolean(onResetToDefault) || stops.length > 0 @@ -217,6 +238,9 @@ export function ComposerModelEffortPickers({ {effortOptions.length > 0 && ( {getEffortLabel(selectedEffort)} )} + {billing?.value === ModelAccessEnumApi.OwnSubscription && ( + ChatGPT plan + )} } @@ -256,6 +280,46 @@ export function ComposerModelEffortPickers({ )} + {billing && ( + { + billing.onChange(value as ModelAccessEnumApi) + setOpen(false) + }} + footer={ + billing.planConnected || billing.locked ? undefined : ( + <> + + selectAndClose(billing.onConnectPlan)} + data-attr="composer-codex-connect-plan" + > + Connect your ChatGPT account + + + ) + } + > + {[ModelAccessEnumApi.PosthogGateway, ModelAccessEnumApi.OwnSubscription].map( + (value) => ( + + {BILLING_LABELS[value]} + + ) + )} + + )} + { + let logic: ReturnType + + beforeEach(() => { + localStorage.clear() + useMocks({ + get: { '/api/users/@me/integrations/codex/': { status: 'not_connected' } }, + post: { + '/api/users/@me/integrations/codex/': { status: 'connected', email: 'jane@example.com' }, + }, + }) + initKeaTests() + logic = codexBillingLogic() + logic.mount() + }) + + afterEach(() => { + logic.unmount() + }) + + it.each([ + ['from the billing row', true, ModelAccessEnumApi.OwnSubscription], + ['from settings', false, ModelAccessEnumApi.PosthogGateway], + ])('picks the billing after a connect started %s', async (_name, fromBillingRow, expected) => { + await expectLogic(logic).toFinishAllListeners() + + if (fromBillingRow) { + logic.actions.connectPlan('composer:test') + } else { + personalCodexIntegrationLogic.actions.openConnectModal('settings') + } + personalCodexIntegrationLogic.actions.pasteAuthFile(AUTH_FILE) + + await expectLogic(logic).toDispatchActions(['connectCodexSuccess']).toFinishAllListeners() + + expect(logic.values.planConnected).toBe(true) + expect(logic.values.effectiveCodexModelAccess).toBe(expected) + }) + + describe('codexModelAccessForRun', () => { + const FLAG = FEATURE_FLAGS.POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD + + it.each([ + ['the flag is off', false, RuntimeAdapterEnumApi.Codex, 'connected', null], + [ + 'the run is on Claude', + true, + RuntimeAdapterEnumApi.Claude, + 'connected', + ModelAccessEnumApi.PosthogGateway, + ], + [ + 'the account is connected', + true, + RuntimeAdapterEnumApi.Codex, + 'connected', + ModelAccessEnumApi.OwnSubscription, + ], + [ + 'the account was disconnected', + true, + RuntimeAdapterEnumApi.Codex, + 'not_connected', + ModelAccessEnumApi.PosthogGateway, + ], + ])( + 'with the plan saved, when %s, resolves before the picker mounts', + async (_name, flagOn, adapter, status, expected) => { + logic.actions.setPreferredCodexModelAccess(ModelAccessEnumApi.OwnSubscription) + logic.unmount() + featureFlagLogic.actions.setFeatureFlags(flagOn ? [FLAG] : [], flagOn ? { [FLAG]: true } : {}) + useMocks({ get: { '/api/users/@me/integrations/codex/': { status } } }) + + expect(codexBillingLogic.findMounted()).toBeNull() + await expect(codexModelAccessForRun(adapter)).resolves.toBe(expected) + logic.mount() + } + ) + }) + + it('leaves a resume billing off while the connection loads with the plan saved', async () => { + const FLAG = FEATURE_FLAGS.POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD + logic.actions.setPreferredCodexModelAccess(ModelAccessEnumApi.OwnSubscription) + logic.unmount() + featureFlagLogic.actions.setFeatureFlags([FLAG], { [FLAG]: true }) + let finishLoad!: () => void + const loaded = new Promise((resolve) => { + finishLoad = resolve + }) + useMocks({ + get: { + '/api/users/@me/integrations/codex/': async () => { + await loaded + return [200, { status: 'connected' }] + }, + }, + }) + logic = codexBillingLogic() + logic.mount() + expect(codexBillingLogic.findMounted()).not.toBeNull() + + expect(pickedCodexModelAccess(RuntimeAdapterEnumApi.Codex)).toBeNull() + expect(pickedCodexModelAccess(RuntimeAdapterEnumApi.Claude)).toBe(ModelAccessEnumApi.PosthogGateway) + + finishLoad() + await waitFor(() => + expect(pickedCodexModelAccess(RuntimeAdapterEnumApi.Codex)).toBe(ModelAccessEnumApi.OwnSubscription) + ) + }) +}) diff --git a/products/posthog_ai/frontend/logics/codexBillingLogic.ts b/products/posthog_ai/frontend/logics/codexBillingLogic.ts new file mode 100644 index 000000000000..d9f99832d3fb --- /dev/null +++ b/products/posthog_ai/frontend/logics/codexBillingLogic.ts @@ -0,0 +1,191 @@ +import { MakeLogicType, actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import posthog from 'posthog-js' + +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { personalCodexIntegrationLogic } from 'scenes/settings/user/personalCodexIntegrationLogic' + +import { usersIntegrationsCodexRetrieve } from '~/generated/core/api' +import { CodexIntegrationStatusEnumApi, type UserCodexIntegrationApi } from '~/generated/core/api.schemas' + +import { ModelAccessEnumApi, RuntimeAdapterEnumApi } from 'products/tasks/frontend/generated/api.schemas' + +// Generated by kea-typegen. Update if you're an agent, ignore if you're human. +export interface codexBillingLogicValues { + codexIntegration: UserCodexIntegrationApi | null // personalCodexIntegrationLogic + effectiveCodexModelAccess: ModelAccessEnumApi + planConnected: boolean + preferredCodexModelAccess: ModelAccessEnumApi + selectPlanAfterConnect: boolean +} + +// Generated by kea-typegen. Update if you're an agent, ignore if you're human. +export interface codexBillingLogicActions { + closeConnectModal: () => { + value: true + } // personalCodexIntegrationLogic + connectCodexSuccess: ( + codexIntegration: UserCodexIntegrationApi, + payload?: import('~/generated/core/api.schemas').UserCodexAuthTokensApi | undefined + ) => { + codexIntegration: UserCodexIntegrationApi + payload?: import('~/generated/core/api.schemas').UserCodexAuthTokensApi + } // personalCodexIntegrationLogic + openConnectModal: (opener: string) => { + opener: string + } // personalCodexIntegrationLogic + connectPlan: (opener: string) => { + opener: string + } + setPreferredCodexModelAccess: (codexModelAccess: ModelAccessEnumApi) => { + codexModelAccess: ModelAccessEnumApi + } +} + +// Generated by kea-typegen. Update if you're an agent, ignore if you're human. +export interface codexBillingLogicMeta { + __keaTypeGenInternalSelectorTypes: { + planConnected: (codexIntegration: UserCodexIntegrationApi | null) => boolean + effectiveCodexModelAccess: ( + preferredCodexModelAccess: ModelAccessEnumApi, + planConnected: boolean + ) => ModelAccessEnumApi + } +} + +export type codexBillingLogicType = MakeLogicType< + codexBillingLogicValues, + codexBillingLogicActions, + Record, + codexBillingLogicMeta +> + +export const codexBillingLogic = kea([ + path(['products', 'posthog_ai', 'frontend', 'logics', 'codexBillingLogic']), + + connect(() => ({ + values: [personalCodexIntegrationLogic, ['codexIntegration']], + actions: [personalCodexIntegrationLogic, ['openConnectModal', 'closeConnectModal', 'connectCodexSuccess']], + })), + + actions({ + setPreferredCodexModelAccess: (codexModelAccess: ModelAccessEnumApi) => ({ codexModelAccess }), + connectPlan: (opener: string) => ({ opener }), + }), + + reducers({ + preferredCodexModelAccess: [ + ModelAccessEnumApi.PosthogGateway as ModelAccessEnumApi, + { persist: true }, + { + setPreferredCodexModelAccess: (_, { codexModelAccess }) => codexModelAccess, + }, + ], + // Cleared by the plan pick, not by `connectCodexSuccess`: reducers run before listeners, so clearing it + // there would hide it from the listener that makes the pick. + selectPlanAfterConnect: [ + false, + { + connectPlan: () => true, + closeConnectModal: () => false, + setPreferredCodexModelAccess: () => false, + }, + ], + }), + + selectors({ + planConnected: [ + (s) => [s.codexIntegration], + (codexIntegration: UserCodexIntegrationApi | null): boolean => + codexIntegration?.status === CodexIntegrationStatusEnumApi.Connected, + ], + effectiveCodexModelAccess: [ + (s) => [s.preferredCodexModelAccess, s.planConnected], + (preferred: ModelAccessEnumApi, planConnected: boolean): ModelAccessEnumApi => + planConnected ? preferred : ModelAccessEnumApi.PosthogGateway, + ], + }), + + listeners(({ actions, values }) => ({ + setPreferredCodexModelAccess: ({ codexModelAccess }) => { + posthog.capture('codex billing changed', { billing: codexModelAccess }) + }, + connectPlan: ({ opener }) => { + actions.openConnectModal(opener) + }, + connectCodexSuccess: () => { + if (values.selectPlanAfterConnect) { + actions.setPreferredCodexModelAccess(ModelAccessEnumApi.OwnSubscription) + } + }, + })), +]) + +function codexBillingEnabled(): boolean { + return !!featureFlagLogic.values.featureFlags[FEATURE_FLAGS.POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD] +} + +/** Whether a run on this harness bills to the ChatGPT plan, as far as the mounted picker knows now. */ +export function usesChatGptPlan(runtimeAdapter: string | null | undefined): boolean { + return ( + codexBillingEnabled() && + runtimeAdapter === RuntimeAdapterEnumApi.Codex && + codexBillingLogic.findMounted()?.values.effectiveCodexModelAccess === ModelAccessEnumApi.OwnSubscription + ) +} + +/** + * The billing the mounted picker shows now, or `null` to leave the field off. A resume starts from the run + * composer, whose picker is mounted, so this is enough there and keeps the send synchronous. While the + * ChatGPT connection still loads, a saved plan is not known to be usable yet, so the field stays off and the + * resumed run keeps the billing of the run it continues. + */ +export function pickedCodexModelAccess(runtimeAdapter: string | null | undefined): ModelAccessEnumApi | null { + if (!codexBillingEnabled()) { + return null + } + if (runtimeAdapter !== RuntimeAdapterEnumApi.Codex) { + return ModelAccessEnumApi.PosthogGateway + } + const billing = codexBillingLogic.findMounted() + if ( + !billing || + (billing.values.codexIntegration === null && + billing.values.preferredCodexModelAccess === ModelAccessEnumApi.OwnSubscription) + ) { + return null + } + return billing.values.effectiveCodexModelAccess +} + +/** + * The billing a new run must state, or `null` to leave the field off because the choice is not offered. + * + * When it is offered the choice is always sent: a resumed run otherwise keeps the billing of the run it + * continues, and the backend refuses a ChatGPT plan on Claude. A seeded task can submit before the picker + * mounts, so this reads the saved choice and the ChatGPT connection itself. + */ +export async function codexModelAccessForRun( + runtimeAdapter: string | null | undefined +): Promise { + if (!codexBillingEnabled()) { + return null + } + if (runtimeAdapter !== RuntimeAdapterEnumApi.Codex) { + return ModelAccessEnumApi.PosthogGateway + } + const unmount = codexBillingLogic.mount() + try { + if (codexBillingLogic.values.preferredCodexModelAccess !== ModelAccessEnumApi.OwnSubscription) { + return ModelAccessEnumApi.PosthogGateway + } + const integration = codexBillingLogic.values.codexIntegration ?? (await usersIntegrationsCodexRetrieve('@me')) + return integration.status === CodexIntegrationStatusEnumApi.Connected + ? ModelAccessEnumApi.OwnSubscription + : ModelAccessEnumApi.PosthogGateway + } catch { + return ModelAccessEnumApi.PosthogGateway + } finally { + unmount() + } +} diff --git a/products/posthog_ai/frontend/logics/runInteractionLogic.ts b/products/posthog_ai/frontend/logics/runInteractionLogic.ts index 79382aca0cc9..0d8f2b7c4663 100644 --- a/products/posthog_ai/frontend/logics/runInteractionLogic.ts +++ b/products/posthog_ai/frontend/logics/runInteractionLogic.ts @@ -53,6 +53,7 @@ import type { PendingAttachment } from '../utils/attachments' import { contextItemLine, wrapWithPosthogContext } from '../utils/posthogContextBlock' import { submitWithWarmRunRetry } from '../utils/warmRunSubmission' import { attachedContextLogic } from './attachedContextLogic' +import { codexBillingLogic, pickedCodexModelAccess, usesChatGptPlan } from './codexBillingLogic' import { composerAttachmentsLogic } from './composerAttachmentsLogic' import { modelCatalogueLogic } from './modelCatalogueLogic' import { type CancellationState, runCancellationLogic } from './runCancellationLogic' @@ -89,6 +90,8 @@ export interface RunInteractionLogicProps { currentMode?: string | null /** The harness the run booted on. Authoritative — a live run can't be moved to another one. */ currentRuntimeAdapter?: string | null + /** Who pays for the run's Codex model use, from the run state. A live run keeps it. */ + currentCodexModelAccess?: string | null /** Called with the new run's id after a terminal-run send starts a fresh run, so the surface can * re-point selection to it (the run lifecycle / selection is a tasks-scene concern, injected here). */ onRunStarted?: (runId: string, handoff?: RunContinuationHandoff) => void @@ -1175,8 +1178,10 @@ export const runInteractionLogic = kea([ values.selectedMode, { resume_from_run_id: props.runId } ) + // A run on the ChatGPT plan boots cold, so its draft releases the warm like an empty one. getWarmLogic()?.actions.noteDraft( - Boolean(values.composerForm.draft.trim()), + Boolean(values.composerForm.draft.trim()) && + !usesChatGptPlan(getRuntimeAdapterForModel(values.catalogue, values.selectedModel)), createRequest as WarmTaskResumeRequestApi ) } @@ -1483,6 +1488,7 @@ export const runInteractionLogic = kea([ setEffort: noteTerminalDraft, setMode: noteTerminalDraft, + [codexBillingLogic.actionTypes.setPreferredCodexModelAccess]: noteTerminalDraft, setComposerFormValue: noteTerminalDraft, setComposerFormValues: noteTerminalDraft, handleTerminalStatus: () => { @@ -1535,6 +1541,9 @@ export const runInteractionLogic = kea([ // from the finished run so the new run continues the thread, and carrying the picked model / // reasoning effort (the resume schema can't, so we send the Claude create shape). The response // carries the new run as `run`; the consumer-provided `onRunStarted` re-points to it. + const codexModelAccess = pickedCodexModelAccess( + getRuntimeAdapterForModel(values.catalogue, values.selectedModel) + ) const createRequest = buildRunCreateRequest( values.catalogue, values.selectedModel, @@ -1543,6 +1552,7 @@ export const runInteractionLogic = kea([ { resume_from_run_id: props.runId, pending_user_message: wrapWithPosthogContext(content, pendingContext), + ...(codexModelAccess ? { codex_model_access: codexModelAccess } : {}), } ) // The task already exists here, so staged artifacts hold the files whether this request diff --git a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskComposer.tsx b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskComposer.tsx index 61a4b995176a..d9dac524a415 100644 --- a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskComposer.tsx +++ b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskComposer.tsx @@ -2,6 +2,7 @@ import { useActions, useMountedLogic, useValues } from 'kea' import { router } from 'kea-router' import { useMemo, useRef } from 'react' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { AIConsentPopoverWrapper } from 'scenes/settings/organization/AIConsentPopoverWrapper' import { urls } from 'scenes/urls' @@ -27,7 +28,11 @@ import { import { AttachedContextBar } from '../../../components/composer/AttachedContextBar' import { ComposerAttachments, useComposerAttachmentPaste } from '../../../components/composer/ComposerAttachments' -import { ComposerModelEffortPickers } from '../../../components/composer/ComposerModelEffortPickers' +import { ComposerCodexBillingPickers } from '../../../components/composer/ComposerCodexBillingPickers' +import { + ComposerModelEffortPickers, + type ComposerModelEffortPickersProps, +} from '../../../components/composer/ComposerModelEffortPickers' import { ComposerModePicker } from '../../../components/composer/ComposerModePicker' import { ComposerModeShortcut } from '../../../components/composer/ComposerModeShortcut' import { useDebouncedDraft } from '../../../components/composer/useDebouncedDraft' @@ -87,6 +92,33 @@ export function TaskComposer({ variant = 'page' }: TaskComposerProps): JSX.Eleme } } + const codexBillingEnabled = useFeatureFlag('POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD') + const modelPickerProps: ComposerModelEffortPickersProps = { + models: offeredModels, + selectedModel: displayModel, + defaultModel, + isDefaultModelLoading: myConfigLoading, + selectedEffort: displayEffort, + isDefaultSelection, + onModelChange: (model) => + setNewTaskData({ + model, + reasoningEffort: resolveEffortForModel(catalogue, newTaskData.reasoningEffort, model), + // Clamp the mode too, not just the effort: leaving a Claude-only mode selected against a Codex + // model would show one permission ceiling and send a broader one. + permissionMode: resolveModeForRuntimeAdapter( + getRuntimeAdapterForModel(catalogue, model), + newTaskData.permissionMode + ), + }), + onEffortChange: (reasoningEffort) => setNewTaskData({ reasoningEffort }), + // Clearing both pins is what hands the choice back to the resolved default — submit then omits the + // triple entirely. + onResetToDefault: () => setNewTaskData({ model: null, reasoningEffort: null }), + onOpenDefaultSettings: () => + router.actions.push(urls.settings('environment-task-agents', 'task-agent-my-preference')), + } + return (
setNewTaskData({ permissionMode })} /> - - setNewTaskData({ - model, - reasoningEffort: resolveEffortForModel( - catalogue, - newTaskData.reasoningEffort, - model - ), - // Clamp the mode too, not just the effort: leaving a - // Claude-only mode selected against a Codex model would - // show one permission ceiling and send a broader one. - permissionMode: resolveModeForRuntimeAdapter( - getRuntimeAdapterForModel(catalogue, model), - newTaskData.permissionMode - ), - }) - } - onEffortChange={(reasoningEffort) => setNewTaskData({ reasoningEffort })} - // Clearing both pins is what hands the choice back to the resolved - // default — submit then omits the triple entirely. - onResetToDefault={() => setNewTaskData({ model: null, reasoningEffort: null })} - onOpenDefaultSettings={() => - router.actions.push( - urls.settings('environment-task-agents', 'task-agent-my-preference') - ) - } - /> + {codexBillingEnabled ? ( + + ) : ( + + )} {/* Open-group state is shared with the side panel; a group left open there would list generic prompts here. */} diff --git a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunChat.tsx b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunChat.tsx index c1537ab4489c..819e835be73c 100644 --- a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunChat.tsx +++ b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunChat.tsx @@ -92,6 +92,10 @@ export function TaskRunChat({ ? runConfig.state.initial_permission_mode : pendingInteraction?.props.currentMode, currentRuntimeAdapter: runConfig?.runtime_adapter ?? pendingInteraction?.props.currentRuntimeAdapter, + currentCodexModelAccess: + typeof runConfig?.state?.codex_model_access === 'string' + ? runConfig.state.codex_model_access + : pendingInteraction?.props.currentCodexModelAccess, onRunStarted: (newRunId, handoff) => { if (handoff) { continueWithRun(handoff) diff --git a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunComposer.tsx b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunComposer.tsx index 178b19a5cb4f..235e93bf03a6 100644 --- a/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunComposer.tsx +++ b/products/posthog_ai/frontend/scenes/TaskTracker/components/TaskRunComposer.tsx @@ -2,6 +2,7 @@ import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { type MutableRefObject, type RefObject, useEffect, useMemo, useRef } from 'react' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { projectLogic } from 'scenes/projectLogic' import { AIConsentPopoverWrapper } from 'scenes/settings/organization/AIConsentPopoverWrapper' import { urls } from 'scenes/urls' @@ -14,12 +15,17 @@ import { runSlashCommandsLogic } from 'products/posthog_ai/frontend/logics/runSl import { taskRunDefaultsLogic } from 'products/posthog_ai/frontend/logics/taskRunDefaultsLogic' import { getRuntimeAdapterForModel, pickerModels } from 'products/posthog_ai/frontend/utils/composerModels' import { cycleMode, getModesForRuntimeAdapter } from 'products/posthog_ai/frontend/utils/composerModes' +import { ModelAccessEnumApi } from 'products/tasks/frontend/generated/api.schemas' import { AttachedContextBar } from '../../../components/composer/AttachedContextBar' import { CommandResultCard } from '../../../components/composer/CommandResultCard' import { ComposerAttachments, useComposerAttachmentPaste } from '../../../components/composer/ComposerAttachments' +import { ComposerCodexBillingPickers } from '../../../components/composer/ComposerCodexBillingPickers' import { ComposerCommandMenu } from '../../../components/composer/ComposerCommandMenu' -import { ComposerModelEffortPickers } from '../../../components/composer/ComposerModelEffortPickers' +import { + ComposerModelEffortPickers, + type ComposerModelEffortPickersProps, +} from '../../../components/composer/ComposerModelEffortPickers' import { ComposerModePicker } from '../../../components/composer/ComposerModePicker' import { ComposerModeShortcut } from '../../../components/composer/ComposerModeShortcut' import { useDebouncedDraft } from '../../../components/composer/useDebouncedDraft' @@ -99,6 +105,22 @@ export function TaskRunComposer({ // The whole input frame is the drop target, so a file dropped anywhere on it attaches. const frameRef = useRef(null) + const codexBillingEnabled = useFeatureFlag('POSTHOG_CODE_CODEX_OWN_SUBSCRIPTION_CLOUD') + const modelPickerProps: ComposerModelEffortPickersProps = { + models: offeredModels, + selectedModel, + defaultModel, + isDefaultModelLoading: myConfigLoading, + selectedEffort, + onModelChange: setModel, + onEffortChange: setEffort, + // While the run is live its harness is fixed to whatever the sandbox booted; once terminal the next send + // starts a fresh run, which may pick any harness. + lockedRuntimeAdapter: isTerminal ? null : logicProps.currentRuntimeAdapter, + onOpenDefaultSettings: () => + router.actions.push(urls.settings('environment-task-agents', 'task-agent-my-preference')), + } + return (
setComposerFocused(true)} onBlurCapture={() => setComposerFocused(false)}> - - router.actions.push( - urls.settings('environment-task-agents', 'task-agent-my-preference') - ) - } - /> + {codexBillingEnabled ? ( + + ) : ( + + )}
diff --git a/products/posthog_ai/frontend/scenes/TaskTracker/taskTrackerSceneLogic.ts b/products/posthog_ai/frontend/scenes/TaskTracker/taskTrackerSceneLogic.ts index a3eeb2e6d628..f04dda6fa001 100644 --- a/products/posthog_ai/frontend/scenes/TaskTracker/taskTrackerSceneLogic.ts +++ b/products/posthog_ai/frontend/scenes/TaskTracker/taskTrackerSceneLogic.ts @@ -17,6 +17,7 @@ import { type ClaudeTaskRunCreateSchemaApi, type CodexTaskRunCreateSchemaApi, type LegacyDesktopAccessResponseApi, + ModelAccessEnumApi, type ModelChoiceApi, TaskOriginProductEnumApi, ReasoningEffortEnumApi, @@ -30,6 +31,7 @@ import type { IntegrationType } from '../../../../../frontend/src/types' import { attachedContextItemKey, attachedContextLogic, runStreamLogic } from '../../api/logics' import type { SuggestionGroup, SuggestionItem } from '../../api/primitives' import { DEFAULT_HEADLINES, pickHeadline } from '../../api/primitives' +import { codexBillingLogic, codexModelAccessForRun, usesChatGptPlan } from '../../logics/codexBillingLogic' import { composerAttachmentsLogic } from '../../logics/composerAttachmentsLogic' import { composerOverrideLogic } from '../../logics/composerOverrideLogic' import type { ComposerOverride } from '../../logics/composerOverrideLogic' @@ -315,6 +317,9 @@ export interface taskTrackerSceneLogicActions { maybeAutoSelectIntegration: () => { value: true } + noteNewTaskDraft: () => { + value: true + } openExistingTask: (task: Task) => { task: Task } @@ -468,6 +473,7 @@ export const taskTrackerSceneLogic = kea([ clearConsentBlock: true, // Pulls any pending `composerSeedLogic` seed into the composer (prefill + optional auto-submit). applyComposerSeed: true, + noteNewTaskDraft: true, }), reducers({ @@ -637,16 +643,26 @@ export const taskTrackerSceneLogic = kea([ // Consent gates warming as it gates submitting (see `submitNewTask`): a warm boots a cloud // sandbox and clones the selected repository, so it must not run before the organization // accepts AI data processing. - if (!values.activeCreation && values.dataProcessingAccepted) { - const request = buildWarmRequest( - { ...values.newTaskData, repositoryConfig: values.effectiveRepositoryConfig }, - values.catalogue, - values.displayModel, - values.displayEffort - ) - if (request) { - actions.noteDraft(values.newTaskData.description.trim().length > 0, request) - } + actions.noteNewTaskDraft() + }, + // A run on the ChatGPT plan can't use a warm sandbox, so switching the billing re-decides the warm. + [codexBillingLogic.actionTypes.setPreferredCodexModelAccess]: () => { + actions.noteNewTaskDraft() + }, + noteNewTaskDraft: () => { + if (values.activeCreation || !values.dataProcessingAccepted) { + return + } + const request = buildWarmRequest( + { ...values.newTaskData, repositoryConfig: values.effectiveRepositoryConfig }, + values.catalogue, + values.displayModel, + values.displayEffort + ) + if (request) { + // An empty draft releases the warm, and so does the ChatGPT plan, which boots cold. + const hasText = values.newTaskData.description.trim().length > 0 + actions.noteDraft(hasText && !usesChatGptPlan(values.composerAdapter), request) } }, // Restore the remembered repo (or fall back to the first connected GitHub integration) when nothing is @@ -701,6 +717,7 @@ export const taskTrackerSceneLogic = kea([ const description = values.newTaskData.description.trim() const { permissionMode } = values.newTaskData const repositoryConfig = values.effectiveRepositoryConfig + const composerAdapter = values.composerAdapter if (!description) { lemonToast.error('Description is required') @@ -740,6 +757,7 @@ export const taskTrackerSceneLogic = kea([ currentMode: permissionMode, currentRuntimeAdapter: values.isDefaultSelection && !values.defaultRuntimeAdapter ? null : values.composerAdapter, + currentCodexModelAccess: usesChatGptPlan(composerAdapter) ? ModelAccessEnumApi.OwnSubscription : null, contextItems: props.contextItems, }) cache.disposables.add( @@ -765,6 +783,8 @@ export const taskTrackerSceneLogic = kea([ ) try { + const codexModelAccess = await codexModelAccessForRun(composerAdapter) + const onChatGptPlan = codexModelAccess === ModelAccessEnumApi.OwnSubscription // Files can only be uploaded against something that already exists. A warm lease names a task // and a run, so they go onto that run and ride its activation. Without one there is nothing // to upload to yet, so warm reuse is given up and the files are staged on the cold task. @@ -789,6 +809,7 @@ export const taskTrackerSceneLogic = kea([ branch: repositoryConfig.branch ?? null, mode: TaskExecutionModeEnumApi.Interactive, pending_user_message: pendingUserMessage, + ...(codexModelAccess ? { codex_model_access: codexModelAccess } : {}), } // An untouched selection pins nothing: the backend resolves the model triple from the // stored team/user default (correct even while the defaults fetch is in flight or has @@ -796,7 +817,9 @@ export const taskTrackerSceneLogic = kea([ // was provisioned the same way, so provisioning and matching resolve alike. An explicit // pick sends the full displayed selection, runtime derived from the model. let pinnedRequest: ClaudeTaskRunCreateSchemaApi | CodexTaskRunCreateSchemaApi | null = null - if (!values.isDefaultSelection) { + // A run on the ChatGPT plan pins the displayed Codex model, so the server can't resolve a default + // on another harness that the plan can't pay for. + if (!values.isDefaultSelection || onChatGptPlan) { const built = buildRunCreateRequest( values.catalogue, values.displayModel, @@ -823,7 +846,8 @@ export const taskTrackerSceneLogic = kea([ // (even `null`) or reuse is never attempted at all — which is how lease-less attachments // opt out. The model triple is left off when the selection is untouched, so the backend // resolves it for warm matching too. - ...(suppressWarmReuse ? {} : { branch: runPayload.branch }), + // A warm sandbox holds no ChatGPT token, so a run on the plan skips warm reuse and boots cold. + ...(suppressWarmReuse || onChatGptPlan ? {} : { branch: runPayload.branch }), ...(pendingUserArtifactIds.length > 0 ? { pending_user_artifact_ids: pendingUserArtifactIds } : {}), ...(pinnedRequest ? { @@ -914,6 +938,10 @@ export const taskTrackerSceneLogic = kea([ ? createdRun.state.initial_permission_mode : interaction.props.currentMode, currentRuntimeAdapter: createdRun?.runtime_adapter ?? interaction.props.currentRuntimeAdapter, + currentCodexModelAccess: + typeof createdRun?.state?.codex_model_access === 'string' + ? createdRun.state.codex_model_access + : (codexModelAccess ?? interaction.props.currentCodexModelAccess), }) // Attach the real ids to the optimistic creation so the detail page adopts this seeded stream // (same `streamKey` + real `runId`) instead of cold-bootstrapping a fresh, skeleton-flashing one.