@taskforcehq/taskforce 0.3.305 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ interface EditableAvatarButtonProps {
3
+ label: string;
4
+ imageUrl?: string | null;
5
+ fallback?: React.ReactNode;
6
+ accentColor?: string | null;
7
+ size?: number;
8
+ width?: number;
9
+ height?: number;
10
+ radius?: number | string;
11
+ editBadgeSize?: number | string;
12
+ editIconSize?: number;
13
+ disabled?: boolean;
14
+ className?: string;
15
+ onClick: () => void;
16
+ }
17
+ export declare function EditableAvatarButton({ label, imageUrl, fallback, accentColor, size, width, height, radius, editBadgeSize, editIconSize, disabled, className, onClick, }: EditableAvatarButtonProps): import("react/jsx-runtime").JSX.Element;
18
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pencil } from 'lucide-react';
3
+ import styles from './EditableAvatarButton.module.css';
4
+ export function EditableAvatarButton({ label, imageUrl, fallback, accentColor, size = 52, width, height, radius, editBadgeSize, editIconSize = 16, disabled = false, className = '', onClick, }) {
5
+ const normalizedImageUrl = String(imageUrl || '').trim();
6
+ const resolvedWidth = width ?? size;
7
+ const resolvedHeight = height ?? size;
8
+ const resolvedRadius = typeof radius === 'number' ? `${radius}px` : radius;
9
+ const resolvedEditBadgeSize = typeof editBadgeSize === 'number' ? `${editBadgeSize}px` : editBadgeSize;
10
+ return (_jsxs("button", { type: "button", className: `${styles.editableAvatarButton} ${className}`.trim(), "aria-label": label, title: label, onClick: onClick, disabled: disabled, style: {
11
+ ['--editable-avatar-size']: `${size}px`,
12
+ ['--editable-avatar-width']: `${resolvedWidth}px`,
13
+ ['--editable-avatar-height']: `${resolvedHeight}px`,
14
+ ...(resolvedRadius ? { ['--editable-avatar-radius']: resolvedRadius } : {}),
15
+ ...(resolvedEditBadgeSize ? { ['--editable-avatar-badge-size']: resolvedEditBadgeSize } : {}),
16
+ ...(accentColor ? { ['--editable-avatar-accent']: accentColor } : {})
17
+ }, children: [normalizedImageUrl ? (_jsx("img", { src: normalizedImageUrl, alt: "", className: styles.editableAvatarImage })) : (_jsx("span", { className: styles.editableAvatarFallback, children: fallback })), _jsx("span", { className: styles.editableAvatarBadge, "aria-hidden": "true", children: _jsx(Pencil, { size: editIconSize }) })] }));
18
+ }
@@ -4,6 +4,7 @@ export type BillingPricingType = 'stripe' | 'free';
4
4
  export interface PublicPricingPlan {
5
5
  planId: string;
6
6
  displayName: string;
7
+ description?: string | null;
7
8
  status: string;
8
9
  defaultVersion: {
9
10
  planVersionId: string;
@@ -15,6 +16,10 @@ export interface PublicPricingPlan {
15
16
  featureKey: string;
16
17
  access: string;
17
18
  config?: Record<string, unknown>;
19
+ label?: string | null;
20
+ description?: string | null;
21
+ publicLabel?: string | null;
22
+ publicDescription?: string | null;
18
23
  }>;
19
24
  pricing: {
20
25
  month: {
@@ -55,6 +60,7 @@ interface PlanComparisonPageProps {
55
60
  currentPlanId?: string | null;
56
61
  currentPlanVersionId?: string | null;
57
62
  currentEntitlementState?: string | null;
63
+ markCurrentPlan?: boolean;
58
64
  canManageCurrentPlan?: boolean;
59
65
  pricingEndpoint?: string;
60
66
  statusBanner?: {
@@ -67,8 +73,10 @@ interface PlanComparisonPageProps {
67
73
  topActions?: React.ReactNode;
68
74
  shellOwnsScroll?: boolean;
69
75
  showPageBackButton?: boolean;
76
+ showHeaderBarWithChrome?: boolean;
77
+ currentPlanCardActionMode?: 'manage' | 'indicator' | 'hidden';
70
78
  fallbackPricingSource?: PricingSourceDetails | null;
71
79
  onSelectPlan: (intent: PlanSelectionIntent) => void | Promise<void>;
72
80
  }
73
- export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
81
+ export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, showHeaderBarWithChrome, currentPlanCardActionMode, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
74
82
  export {};
@@ -61,7 +61,49 @@ function getPlanSortRank(plan) {
61
61
  return Math.min(...numericAmounts);
62
62
  return Number.POSITIVE_INFINITY;
63
63
  }
64
- export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, fallbackPricingSource = null, onSelectPlan }) {
64
+ function formatFeatureKeyFallback(featureKey) {
65
+ return String(featureKey || '')
66
+ .replace(/[_\-.]+/g, ' ')
67
+ .replace(/\b\w/g, (c) => c.toUpperCase());
68
+ }
69
+ function readConfiguredFeatureLimit(config, key) {
70
+ const rawValue = config?.[key];
71
+ const numericValue = Number(rawValue);
72
+ if (!Number.isFinite(numericValue))
73
+ return null;
74
+ const normalized = Math.max(1, Math.floor(numericValue));
75
+ return normalized;
76
+ }
77
+ function getFeatureDisplayParts(feature, seatLimit) {
78
+ const baseLabel = String(feature.publicLabel || feature.label || '').trim() || formatFeatureKeyFallback(feature.featureKey);
79
+ const config = feature.config && typeof feature.config === 'object' ? feature.config : {};
80
+ if (feature.featureKey === 'context.uploads') {
81
+ const storageLimitMb = readConfiguredFeatureLimit(config, 'storageLimitMb');
82
+ if (storageLimitMb !== null) {
83
+ return { limitValue: String(storageLimitMb), limitUnit: 'MB', baseLabel };
84
+ }
85
+ }
86
+ if (feature.featureKey === 'workspace.workspaces') {
87
+ const maxWorkspaces = readConfiguredFeatureLimit(config, 'maxWorkspaces');
88
+ if (maxWorkspaces !== null) {
89
+ return { limitValue: String(maxWorkspaces), limitUnit: 'x', baseLabel };
90
+ }
91
+ }
92
+ if (feature.featureKey === 'collaboration.ai_profiles') {
93
+ const maxAiProfiles = readConfiguredFeatureLimit(config, 'maxAiProfiles');
94
+ if (maxAiProfiles !== null) {
95
+ return { limitValue: String(maxAiProfiles), limitUnit: 'x', baseLabel };
96
+ }
97
+ }
98
+ if (feature.featureKey === 'collaboration.team_management') {
99
+ const normalizedSeatLimit = Number(seatLimit);
100
+ if (Number.isFinite(normalizedSeatLimit) && normalizedSeatLimit > 0) {
101
+ return { limitValue: String(Math.floor(normalizedSeatLimit)), limitUnit: 'x', baseLabel };
102
+ }
103
+ }
104
+ return { limitValue: null, limitUnit: null, baseLabel };
105
+ }
106
+ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
65
107
  const [plans, setPlans] = useState([]);
66
108
  const [pricingSource, setPricingSource] = useState(null);
67
109
  const [loading, setLoading] = useState(true);
@@ -114,6 +156,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
114
156
  return String(a.displayName || a.planId).localeCompare(String(b.displayName || b.planId));
115
157
  }), [plans]);
116
158
  const pricingSourceLabel = useMemo(() => formatPricingSource(pricingSource || fallbackPricingSource), [fallbackPricingSource, pricingSource]);
159
+ const planSelectionEnabled = authSessionResolved;
117
160
  const contentClassName = `${styles.plansWrapper} ${shellOwnsScroll ? styles.shellOwnsScroll : 'tf-scrollbar'} ${variant === 'site' ? styles.siteVariant : ''} ${chrome ? styles.withChrome : ''}`.trim();
118
161
  const wrapPage = (content) => {
119
162
  if (shellOwnsScroll) {
@@ -121,13 +164,13 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
121
164
  }
122
165
  return (_jsxs("div", { className: taskforceStyles.standaloneWrapper, "data-theme": "dark", children: [chrome, content, footer] }));
123
166
  };
124
- if (loading || !authSessionResolved) {
167
+ if (loading) {
125
168
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.loader, children: [_jsx(Loader2, { size: 48, className: styles.spinner }), _jsx("p", { children: "Loading plans..." })] }) }) }) }));
126
169
  }
127
170
  if (error) {
128
171
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.emptyState, children: [_jsx("h2", { children: "Connecting to plan pricing..." }), _jsx("p", { children: error }), _jsx("button", { className: styles.backBtn, onClick: () => setRetryNonce((value) => value + 1), children: "Retry" })] }) }) }) }));
129
172
  }
130
- return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [!chrome && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: heading }), subtitle && _jsx("p", { className: styles.subtitle, children: subtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
173
+ return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: heading }), subtitle && _jsx("p", { className: styles.subtitle, children: subtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
131
174
  ? styles.successBanner
132
175
  : (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
133
176
  const monthPrice = plan.pricing.month;
@@ -136,17 +179,27 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
136
179
  const hasYear = isSelectablePrice(yearPrice);
137
180
  const planVersionId = String(plan.defaultVersion?.planVersionId || '').trim();
138
181
  const hasActiveEntitlement = isCommercialEntitlementActive(currentEntitlementState);
139
- const isCurrent = hasActiveEntitlement && (Boolean(planVersionId
182
+ const shouldMarkCurrentPlan = markCurrentPlan ?? hasActiveEntitlement;
183
+ const hasKnownLinkedPlan = Boolean(planVersionId
140
184
  && currentPlanVersionId
141
- && currentPlanVersionId === planVersionId) || (!currentPlanVersionId
142
- && Boolean(currentPlanId && currentPlanId === plan.planId)));
185
+ && currentPlanVersionId === planVersionId) || Boolean(!currentPlanVersionId
186
+ && Boolean(currentPlanId && currentPlanId === plan.planId));
187
+ const isCurrent = shouldMarkCurrentPlan && hasKnownLinkedPlan && currentEntitlementState !== 'canceled';
143
188
  const defaultInterval = hasMonth ? 'month' : (hasYear ? 'year' : null);
144
189
  const defaultPrice = defaultInterval === 'year' ? yearPrice : monthPrice;
145
- const canSelectCard = Boolean(planVersionId) && (isCurrent ? canManageCurrentPlan : Boolean(defaultInterval));
190
+ const currentPlanShowsManageAction = currentPlanCardActionMode === 'manage';
191
+ const currentPlanShowsIndicator = currentPlanCardActionMode === 'indicator';
192
+ const canSelectCard = planSelectionEnabled
193
+ && Boolean(planVersionId)
194
+ && (isCurrent
195
+ ? (currentPlanShowsManageAction && canManageCurrentPlan)
196
+ : Boolean(defaultInterval));
146
197
  const handleCardSelect = () => {
147
198
  if (!planVersionId)
148
199
  return;
149
200
  if (isCurrent) {
201
+ if (!currentPlanShowsManageAction)
202
+ return;
150
203
  if (!canManageCurrentPlan)
151
204
  return;
152
205
  void onSelectPlan({
@@ -173,23 +226,26 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
173
226
  event.preventDefault();
174
227
  handleCardSelect();
175
228
  }
176
- } : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }), _jsx("div", { className: styles.featuresList, children: Array.isArray(plan.features) && plan.features.length > 0 ? plan.features.map((feature, idx) => (_jsxs("div", { className: styles.featureItem, children: [_jsx(CheckCircle2, { className: styles.featureIcon }), _jsx("span", { className: styles.featureText, children: String(feature.featureKey || '').replace(/[_\-.]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) })] }, `${feature.featureKey}-${idx}`))) : (_jsx("span", { className: styles.featureText, children: "No additional features included" })) }), _jsxs("div", { className: styles.priceRows, children: [_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(monthPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ month" })] }), _jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(yearPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ year" })] })] }), isCurrent ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-manage-${plan.planId}`, onClick: (event) => {
229
+ } : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }), String(plan.description || '').trim() && (_jsx("p", { className: styles.planDescription, children: String(plan.description || '').trim() })), _jsx("div", { className: styles.featuresList, children: Array.isArray(plan.features) && plan.features.length > 0 ? plan.features.map((feature, idx) => (_jsxs("div", { className: styles.featureItem, children: [_jsx(CheckCircle2, { className: styles.featureIcon }), _jsxs("div", { className: styles.featureTextBlock, children: [(() => {
230
+ const { limitValue, limitUnit, baseLabel } = getFeatureDisplayParts(feature, plan.defaultVersion?.seatLimit);
231
+ return (_jsxs("span", { className: styles.featureText, children: [limitValue && (_jsxs("span", { className: styles.featureLimitAccent, children: [_jsx("span", { children: limitValue }), limitUnit ? (_jsx("span", { className: limitUnit === 'MB' ? styles.featureLimitUnit : undefined, children: limitUnit })) : null] })), _jsx("span", { children: limitValue ? ` ${baseLabel}` : baseLabel })] }));
232
+ })(), String(feature.publicDescription || feature.description || '').trim() && (_jsx("span", { className: styles.featureDescription, children: String(feature.publicDescription || feature.description || '').trim() }))] })] }, `${feature.featureKey}-${idx}`))) : (_jsx("span", { className: styles.featureText, children: "No additional features included" })) }), (hasMonth || hasYear) ? (_jsxs("div", { className: styles.priceRows, children: [hasMonth ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(monthPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ month" })] })) : null, hasYear ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(yearPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ year" })] })) : null] })) : null, isCurrent ? (currentPlanShowsManageAction ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-manage-${plan.planId}`, onClick: (event) => {
177
233
  event.stopPropagation();
178
234
  if (!canManageCurrentPlan)
179
235
  return;
180
236
  void onSelectPlan({ planId: plan.planId, planVersionId, interval: defaultInterval || 'month', pricingType: defaultPrice?.pricingType || 'stripe', isCurrentPlan: true });
181
- }, disabled: !planVersionId || !canManageCurrentPlan, children: isAuthenticated
237
+ }, disabled: !planVersionId || !canManageCurrentPlan || !planSelectionEnabled, children: isAuthenticated
182
238
  ? (canManageCurrentPlan ? 'Manage Plan' : 'Current Plan')
183
- : 'Current Plan' })) : (_jsxs("div", { className: styles.ctaRow, children: [_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
239
+ : 'Current Plan' })) : (currentPlanShowsIndicator ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-current-${plan.planId}`, disabled: true, children: "Current Plan" })) : null)) : (_jsxs("div", { className: styles.ctaRow, children: [hasMonth ? (_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
184
240
  event.stopPropagation();
185
241
  void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'month', pricingType: monthPrice?.pricingType || 'stripe', isCurrentPlan: false });
186
- }, disabled: !planVersionId || !hasMonth, children: monthPrice?.pricingType === 'free'
242
+ }, disabled: !planVersionId || !planSelectionEnabled, children: monthPrice?.pricingType === 'free'
187
243
  ? (isAuthenticated ? 'Select Free' : 'Get Started Free')
188
- : (isAuthenticated ? 'Choose Monthly' : 'Get Started Monthly') }), _jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
244
+ : (isAuthenticated ? 'Choose Monthly' : 'Get Started Monthly') })) : null, hasYear ? (_jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
189
245
  event.stopPropagation();
190
246
  void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'year', pricingType: yearPrice?.pricingType || 'stripe', isCurrentPlan: false });
191
- }, disabled: !planVersionId || !hasYear, children: yearPrice?.pricingType === 'free'
247
+ }, disabled: !planVersionId || !planSelectionEnabled, children: yearPrice?.pricingType === 'free'
192
248
  ? (isAuthenticated ? 'Select Free' : 'Get Started Free')
193
- : (isAuthenticated ? 'Choose Yearly' : 'Get Started Yearly') })] }))] }, plan.planId));
249
+ : (isAuthenticated ? 'Choose Yearly' : 'Get Started Yearly') })) : null] }))] }, plan.planId));
194
250
  }) }))] }) }) }));
195
251
  }
@@ -17,6 +17,7 @@ describe('PlanComparisonPage', () => {
17
17
  {
18
18
  planId: 'free',
19
19
  displayName: 'Free',
20
+ description: 'Start using Taskforce at no cost.',
20
21
  status: 'active',
21
22
  defaultVersion: {
22
23
  planVersionId: 'free-v1',
@@ -33,6 +34,7 @@ describe('PlanComparisonPage', () => {
33
34
  {
34
35
  planId: 'team-5',
35
36
  displayName: 'Team 5',
37
+ description: 'For small collaborative teams.',
36
38
  status: 'active',
37
39
  defaultVersion: {
38
40
  planVersionId: 'team-5-v2',
@@ -59,6 +61,11 @@ describe('PlanComparisonPage', () => {
59
61
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
60
62
  expect(await screen.findByText('Pricing source: production (cloud)')).toBeInTheDocument();
61
63
  });
64
+ it('renders plan descriptions when provided by the pricing payload', async () => {
65
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
66
+ expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
67
+ expect(screen.getByText('For small collaborative teams.')).toBeInTheDocument();
68
+ });
62
69
  it('falls back to provided pricing source details when the pricing response omits them', async () => {
63
70
  globalThis.__DEBUG_MODE__ = true;
64
71
  vi.stubGlobal('fetch', vi.fn(async () => ({
@@ -92,6 +99,188 @@ describe('PlanComparisonPage', () => {
92
99
  await screen.findByRole('heading', { name: 'Subscription Plans' });
93
100
  expect(screen.queryByText('Pricing source: production (cloud)')).not.toBeInTheDocument();
94
101
  });
102
+ it('renders public feature copy when provided by the pricing payload', async () => {
103
+ vi.stubGlobal('fetch', vi.fn(async () => ({
104
+ ok: true,
105
+ json: async () => ({
106
+ pricingSource: {
107
+ environment: 'production',
108
+ runtimeMode: 'cloud'
109
+ },
110
+ plans: [
111
+ {
112
+ planId: 'team-5',
113
+ displayName: 'Team 5',
114
+ status: 'active',
115
+ defaultVersion: {
116
+ planVersionId: 'team-5-v2',
117
+ versionNumber: 2,
118
+ seatLimit: 5,
119
+ metadata: {}
120
+ },
121
+ features: [
122
+ {
123
+ featureKey: 'collaboration.ai_profiles',
124
+ access: 'enabled',
125
+ label: 'AI Profiles',
126
+ publicLabel: 'AI teammates',
127
+ publicDescription: 'Add reusable AI teammates to your workspace.'
128
+ }
129
+ ],
130
+ pricing: {
131
+ month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 2000, currency: 'usd', interval: 'month' },
132
+ year: null
133
+ }
134
+ }
135
+ ]
136
+ })
137
+ })));
138
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
139
+ expect(await screen.findByText('AI teammates')).toBeInTheDocument();
140
+ expect(screen.getByText('Add reusable AI teammates to your workspace.')).toBeInTheDocument();
141
+ expect(screen.queryByText('Collaboration Ai Profiles')).not.toBeInTheDocument();
142
+ });
143
+ it('renders inline numeric feature limits next to each feature label', async () => {
144
+ vi.stubGlobal('fetch', vi.fn(async () => ({
145
+ ok: true,
146
+ json: async () => ({
147
+ pricingSource: {
148
+ environment: 'production',
149
+ runtimeMode: 'cloud'
150
+ },
151
+ plans: [
152
+ {
153
+ planId: 'free',
154
+ displayName: 'Free',
155
+ status: 'active',
156
+ defaultVersion: {
157
+ planVersionId: 'free-v1',
158
+ versionNumber: 1,
159
+ seatLimit: 12,
160
+ metadata: {}
161
+ },
162
+ features: [
163
+ {
164
+ featureKey: 'workspace.workspaces',
165
+ access: 'enabled',
166
+ label: 'Workspaces',
167
+ config: { maxWorkspaces: 1 }
168
+ },
169
+ {
170
+ featureKey: 'collaboration.ai_profiles',
171
+ access: 'limited',
172
+ label: 'AI Profiles',
173
+ publicLabel: 'AI Agents',
174
+ config: { maxAiProfiles: 4 }
175
+ },
176
+ {
177
+ featureKey: 'context.uploads',
178
+ access: 'enabled',
179
+ label: 'Cloud Storage',
180
+ config: { storageLimitMb: 24 }
181
+ },
182
+ {
183
+ featureKey: 'collaboration.team_management',
184
+ access: 'enabled',
185
+ label: 'Team Management'
186
+ }
187
+ ],
188
+ pricing: {
189
+ month: { pricingType: 'free', stripePriceId: null, unitAmount: null, currency: null, interval: 'month' },
190
+ year: null
191
+ }
192
+ }
193
+ ]
194
+ })
195
+ })));
196
+ const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
197
+ const freeCard = await screen.findByTestId('plan-card-free');
198
+ const featureTextNodes = Array.from(freeCard.querySelectorAll('span')).map((node) => node.textContent);
199
+ expect(featureTextNodes).toContain('1x Workspaces');
200
+ expect(featureTextNodes).toContain('4x AI Agents');
201
+ expect(featureTextNodes).toContain('24MB Cloud Storage');
202
+ expect(featureTextNodes).toContain('12x Team Management');
203
+ expect(container.querySelector('[class*="featureLimitUnit"]')?.textContent).toBe('MB');
204
+ });
205
+ it('renders the storage unit suffix in a smaller accent span', async () => {
206
+ vi.stubGlobal('fetch', vi.fn(async () => ({
207
+ ok: true,
208
+ json: async () => ({
209
+ pricingSource: {
210
+ environment: 'production',
211
+ runtimeMode: 'cloud'
212
+ },
213
+ plans: [
214
+ {
215
+ planId: 'free',
216
+ displayName: 'Free',
217
+ status: 'active',
218
+ defaultVersion: {
219
+ planVersionId: 'free-v1',
220
+ versionNumber: 1,
221
+ seatLimit: null,
222
+ metadata: {}
223
+ },
224
+ features: [
225
+ {
226
+ featureKey: 'context.uploads',
227
+ access: 'enabled',
228
+ label: 'Cloud Storage',
229
+ config: { storageLimitMb: 64 }
230
+ }
231
+ ],
232
+ pricing: {
233
+ month: { pricingType: 'free', stripePriceId: null, unitAmount: null, currency: null, interval: 'month' },
234
+ year: null
235
+ }
236
+ }
237
+ ]
238
+ })
239
+ })));
240
+ const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
241
+ await screen.findByText('Cloud Storage');
242
+ const accent = container.querySelector('[class*="featureLimitAccent"]');
243
+ const unit = container.querySelector('[class*="featureLimitUnit"]');
244
+ expect(accent?.textContent).toBe('64MB');
245
+ expect(unit?.textContent).toBe('MB');
246
+ });
247
+ it('hides missing yearly pricing rows and buttons instead of rendering unavailable state', async () => {
248
+ vi.stubGlobal('fetch', vi.fn(async () => ({
249
+ ok: true,
250
+ json: async () => ({
251
+ pricingSource: {
252
+ environment: 'production',
253
+ runtimeMode: 'cloud'
254
+ },
255
+ plans: [
256
+ {
257
+ planId: 'operator',
258
+ displayName: 'Operator',
259
+ status: 'active',
260
+ defaultVersion: {
261
+ planVersionId: 'operator-v1',
262
+ versionNumber: 1,
263
+ seatLimit: null,
264
+ metadata: {}
265
+ },
266
+ features: [],
267
+ pricing: {
268
+ month: { pricingType: 'stripe', stripePriceId: 'price_operator_month', unitAmount: 999, currency: 'usd', interval: 'month' },
269
+ year: null
270
+ }
271
+ }
272
+ ]
273
+ })
274
+ })));
275
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
276
+ const operatorCard = await screen.findByTestId('plan-card-operator');
277
+ expect(within(operatorCard).getByText('$9.99')).toBeInTheDocument();
278
+ expect(within(operatorCard).getByText('/ month')).toBeInTheDocument();
279
+ expect(within(operatorCard).queryByText('/ year')).not.toBeInTheDocument();
280
+ expect(within(operatorCard).queryByText('Unavailable')).not.toBeInTheDocument();
281
+ expect(within(operatorCard).getByRole('button', { name: 'Choose Monthly' })).toBeInTheDocument();
282
+ expect(within(operatorCard).queryByRole('button', { name: 'Choose Yearly' })).not.toBeInTheDocument();
283
+ });
95
284
  it('does not mark a plan current when only the plan id matches but the version does not', async () => {
96
285
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v1", currentEntitlementState: "active", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
97
286
  expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
@@ -102,15 +291,64 @@ describe('PlanComparisonPage', () => {
102
291
  expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
103
292
  expect(screen.getByText('Current Plan')).toBeInTheDocument();
104
293
  });
294
+ it('can show the current plan as a non-interactive indicator when billing is managed from page chrome', async () => {
295
+ const onSelectPlan = vi.fn();
296
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v2", currentEntitlementState: "active", canManageCurrentPlan: true, currentPlanCardActionMode: "indicator", onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
297
+ expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
298
+ const currentCard = screen.getByTestId('plan-card-team-5');
299
+ expect(currentCard).not.toHaveAttribute('role', 'button');
300
+ expect(screen.queryByTestId('plan-manage-team-5')).not.toBeInTheDocument();
301
+ expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
302
+ expect(screen.getByTestId('plan-current-team-5')).toBeDisabled();
303
+ fireEvent.click(currentCard);
304
+ expect(onSelectPlan).not.toHaveBeenCalled();
305
+ });
306
+ it('can render the page header bar even when shell chrome is present', async () => {
307
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, chrome: _jsx("div", { children: "Shell chrome" }), showHeaderBarWithChrome: true, topActions: _jsx("button", { type: "button", children: "Manage billing" }), onBack: () => { }, backLabel: "Back to App", onSelectPlan: () => { } }));
308
+ expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
309
+ expect(screen.getByRole('button', { name: 'Back to App' })).toBeInTheDocument();
310
+ expect(screen.getByRole('button', { name: 'Manage billing' })).toBeInTheDocument();
311
+ });
105
312
  it('does not mark a canceled entitlement as current through the plan-id fallback path', async () => {
106
313
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: null, currentEntitlementState: "canceled", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
107
314
  expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
108
315
  expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
109
316
  });
317
+ it('does not mark a linked pending-plan-selection entitlement as the current plan', async () => {
318
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v2", currentEntitlementState: "pending_plan_selection", canManageCurrentPlan: true, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
319
+ expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
320
+ expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
321
+ expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
322
+ expect(screen.getByRole('button', { name: 'Choose Monthly' })).toBeInTheDocument();
323
+ });
110
324
  it('can disable the page back button while onboarding remains blocked', async () => {
111
325
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back to App", backDisabled: true, onSelectPlan: () => { } }));
112
326
  expect(await screen.findByRole('button', { name: 'Back to App' })).toBeDisabled();
113
327
  });
328
+ it('shows pricing before auth resolves but keeps plan selection disabled until bootstrap finishes', async () => {
329
+ const onSelectPlan = vi.fn();
330
+ const view = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
331
+ expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
332
+ const freeCard = screen.getByTestId('plan-card-free');
333
+ expect(screen.getByRole('button', { name: 'Get Started Free' })).toBeDisabled();
334
+ expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeDisabled();
335
+ expect(freeCard).not.toHaveAttribute('role', 'button');
336
+ view.rerender(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: true, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
337
+ const enabledFreeCard = screen.getByTestId('plan-card-free');
338
+ expect(screen.getByRole('button', { name: 'Get Started Free' })).toBeEnabled();
339
+ expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeEnabled();
340
+ expect(enabledFreeCard).toHaveAttribute('role', 'button');
341
+ fireEvent.click(screen.getByRole('button', { name: 'Get Started Free' }));
342
+ await waitFor(() => {
343
+ expect(onSelectPlan).toHaveBeenCalledWith({
344
+ planId: 'free',
345
+ planVersionId: 'free-v1',
346
+ interval: 'month',
347
+ pricingType: 'free',
348
+ isCurrentPlan: false
349
+ });
350
+ });
351
+ });
114
352
  it('selects the whole plan card using the default available interval', async () => {
115
353
  const onSelectPlan = vi.fn();
116
354
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));