@taskforcehq/taskforce 0.3.169 → 0.3.300

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 (696) hide show
  1. package/README.md +52 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2212 -1194
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +66 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +46 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +239 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +119 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3293 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-TVmCgizS.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CAyhnPeR.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-BOCw5e4D.js +250 -0
  492. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  493. package/dist/ui/assets/InitiativesModule-CN0r02Oi.js +1 -0
  494. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  495. package/dist/ui/assets/PlansPage-FcQSNLZd.js +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-CY9R2zPB.js +5 -0
  499. package/dist/ui/assets/index-Bzqy5QGm.js +6 -0
  500. package/dist/ui/assets/index-DxRiI3tY.css +1 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +28 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/migrate-planning-model.ts +233 -0
  610. package/scripts/migrate-task-data-assets.mjs +408 -0
  611. package/scripts/playwright-auth.sh +11 -0
  612. package/scripts/run-dev-proxy.sh +2 -0
  613. package/scripts/update-homebrew-formula.sh +20 -3
  614. package/scripts/with-esbuild.sh +11 -0
  615. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  616. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  617. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  618. package/src/resources/templates/workflows/do.yaml +8 -2
  619. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  620. package/src/resources/templates/workflows/execute.yaml +40 -23
  621. package/src/resources/templates/workflows/plan.yaml +15 -8
  622. package/src/resources/templates/workflows/review.yaml +14 -8
  623. package/dist/TaskforceScreenshot.d.ts +0 -11
  624. package/dist/TaskforceScreenshot.js +0 -113
  625. package/dist/compat/priorityCompat.d.ts +0 -2
  626. package/dist/compat/priorityCompat.js +0 -24
  627. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  628. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  629. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  630. package/dist/components/task/TaskKanban 2.js +0 -1004
  631. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  632. package/dist/components/task/TaskScreenshotSection.js +0 -22
  633. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  634. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  635. package/dist/core/InitiativeTemplates.test.js +0 -50
  636. package/dist/core/ParentLifecycle.test.js +0 -98
  637. package/dist/migrations/clientMigrations.d.ts +0 -9
  638. package/dist/migrations/clientMigrations.js +0 -74
  639. package/dist/migrations/flags.d.ts +0 -6
  640. package/dist/migrations/flags.js +0 -8
  641. package/dist/migrations/taskMigrations.d.ts +0 -71
  642. package/dist/migrations/taskMigrations.js +0 -236
  643. package/dist/migrations/taskMigrations.test.js +0 -34
  644. package/dist/server/routes.billing.js +0 -991
  645. package/dist/server/routes.billing.test.js +0 -387
  646. package/dist/server/routes.sync.integration.test.js +0 -470
  647. package/dist/server/routes.sync.js +0 -1141
  648. package/dist/soak/syncSoakHarness.d.ts +0 -6
  649. package/dist/soak/syncSoakHarness.js +0 -10
  650. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  651. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  652. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  653. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  654. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  655. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  656. package/dist/utils/taskHierarchy.d.ts +0 -4
  657. package/dist/utils/taskHierarchy.js +0 -69
  658. package/dist/utils/taskHierarchy.test.js +0 -38
  659. package/dist/utils/taskParenting.d.ts +0 -3
  660. package/dist/utils/taskParenting.js +0 -24
  661. package/dist/utils/taskParenting.test.js +0 -21
  662. package/dist/utils/taskRelations.d.ts +0 -6
  663. package/dist/utils/taskRelations.js +0 -24
  664. package/dist/utils/taskRelations.test.js +0 -34
  665. package/scripts/check-compat-boundaries.js +0 -48
  666. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  667. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  668. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  669. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  670. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  671. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  672. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  673. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  677. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  678. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  679. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  680. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  681. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  682. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  683. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  684. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  685. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  686. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  687. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  688. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  689. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  690. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  691. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  692. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  693. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  694. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  695. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  696. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,2831 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { AnnotatedAttachmentWorkspaceShell } from './AnnotatedAttachmentWorkspace';
7
+ const { fetchTaskforceApi } = vi.hoisted(() => ({
8
+ fetchTaskforceApi: vi.fn(),
9
+ }));
10
+ vi.mock('../../utils/taskforceApiClient', () => ({
11
+ fetchTaskforceApi,
12
+ resolveTaskforceApiUrl: (path) => path,
13
+ }));
14
+ function buildSessionResponse(overrides) {
15
+ return {
16
+ id: 'session-1',
17
+ tenantId: 'tenant-1',
18
+ workspaceId: 'workspace-1',
19
+ taskId: 'task-1',
20
+ baseImageAssetId: 'asset-image-1',
21
+ title: 'Homepage shot review',
22
+ globalInstruction: '',
23
+ annotations: [],
24
+ createdByActorId: 'user-1',
25
+ updatedByActorId: 'user-1',
26
+ createdAt: '2026-03-25T12:00:00.000Z',
27
+ updatedAt: '2026-03-25T12:00:00.000Z',
28
+ deletedAt: null,
29
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
30
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
31
+ ...overrides,
32
+ };
33
+ }
34
+ function mockAutoCreateFirstSession(sessionOverrides) {
35
+ const session = buildSessionResponse(sessionOverrides);
36
+ fetchTaskforceApi
37
+ .mockResolvedValueOnce({
38
+ ok: true,
39
+ json: async () => ({
40
+ sessions: [],
41
+ }),
42
+ })
43
+ .mockResolvedValueOnce({
44
+ ok: true,
45
+ json: async () => ({
46
+ session,
47
+ }),
48
+ });
49
+ return session;
50
+ }
51
+ function escapeForRegex(value) {
52
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ }
54
+ async function findSessionCard(title) {
55
+ const pattern = new RegExp(escapeForRegex(title), 'i');
56
+ const matchingInput = screen.queryByDisplayValue(pattern);
57
+ if (matchingInput)
58
+ return matchingInput;
59
+ const matchingText = await screen.findByText(pattern);
60
+ return matchingText.closest('button') ?? matchingText;
61
+ }
62
+ describe('AnnotatedAttachmentWorkspaceShell', () => {
63
+ beforeEach(() => {
64
+ fetchTaskforceApi.mockReset();
65
+ if (typeof window.ResizeObserver === 'undefined') {
66
+ class ResizeObserverMock {
67
+ observe() { }
68
+ disconnect() { }
69
+ unobserve() { }
70
+ }
71
+ Object.defineProperty(window, 'ResizeObserver', {
72
+ configurable: true,
73
+ writable: true,
74
+ value: ResizeObserverMock,
75
+ });
76
+ }
77
+ });
78
+ afterEach(() => {
79
+ vi.useRealTimers();
80
+ vi.restoreAllMocks();
81
+ });
82
+ it('shows an image loading indicator until the canvas image finishes loading', async () => {
83
+ fetchTaskforceApi.mockResolvedValueOnce({
84
+ ok: true,
85
+ json: async () => ({
86
+ sessions: [
87
+ {
88
+ id: 'session-1',
89
+ tenantId: 'tenant-1',
90
+ workspaceId: 'workspace-1',
91
+ taskId: 'task-1',
92
+ baseImageAssetId: 'asset-image-1',
93
+ title: 'Homepage review',
94
+ globalInstruction: 'Review this screenshot',
95
+ annotations: [
96
+ {
97
+ id: 'annotation-1',
98
+ order: 0,
99
+ kind: 'pin',
100
+ x: 0.2,
101
+ y: 0.3,
102
+ instruction: 'Inspect this button',
103
+ markerType: 'question',
104
+ color: '#0f766e',
105
+ },
106
+ ],
107
+ createdByActorId: 'user-1',
108
+ updatedByActorId: 'user-1',
109
+ createdAt: '2026-03-25T12:00:00.000Z',
110
+ updatedAt: '2026-03-25T12:00:00.000Z',
111
+ deletedAt: null,
112
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
113
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
114
+ },
115
+ ],
116
+ }),
117
+ });
118
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
119
+ taskId: 'task-1',
120
+ taskReferenceLabel: 'T-101',
121
+ imageReferenceLabel: 'I-22',
122
+ assetId: 'asset-image-1',
123
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
124
+ displayName: 'Homepage shot',
125
+ } }));
126
+ expect(await screen.findByText('Loading image…')).toBeInTheDocument();
127
+ expect(screen.queryByTestId('annotated-attachment-overlay')).not.toBeInTheDocument();
128
+ fireEvent.load(screen.getByAltText('Homepage shot'));
129
+ await waitFor(() => {
130
+ expect(screen.queryByText('Loading image…')).not.toBeInTheDocument();
131
+ });
132
+ expect(screen.getByTestId('annotated-attachment-overlay')).toBeInTheDocument();
133
+ });
134
+ it('auto-creates the first session and still renders the image while it loads', async () => {
135
+ mockAutoCreateFirstSession();
136
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
137
+ taskId: 'task-1',
138
+ taskReferenceLabel: 'T-101',
139
+ assetId: 'asset-image-1',
140
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
141
+ displayName: 'Homepage shot',
142
+ } }));
143
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
144
+ expect(screen.getByText('Loading image…')).toBeInTheDocument();
145
+ fireEvent.load(screen.getByAltText('Homepage shot'));
146
+ await waitFor(() => {
147
+ expect(screen.queryByText('Loading image…')).not.toBeInTheDocument();
148
+ });
149
+ expect(screen.getByAltText('Homepage shot')).toBeInTheDocument();
150
+ expect(screen.getByTestId('annotated-attachment-overlay')).toBeInTheDocument();
151
+ });
152
+ it('warns and offers to create a session when a marker tool is clicked without an active session', async () => {
153
+ const user = userEvent.setup();
154
+ const session = buildSessionResponse({
155
+ id: 'session-recovered-1',
156
+ title: 'Recovered review',
157
+ taskId: 'task-1',
158
+ });
159
+ fetchTaskforceApi
160
+ .mockResolvedValueOnce({
161
+ ok: true,
162
+ json: async () => ({ sessions: [] }),
163
+ })
164
+ .mockResolvedValueOnce({
165
+ ok: false,
166
+ json: async () => ({ error: 'Failed to create session.' }),
167
+ })
168
+ .mockResolvedValueOnce({
169
+ ok: true,
170
+ json: async () => ({ session }),
171
+ })
172
+ .mockResolvedValueOnce({
173
+ ok: true,
174
+ json: async () => ({ sessions: [session] }),
175
+ });
176
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
177
+ taskId: 'task-1',
178
+ taskReferenceLabel: 'T-101',
179
+ assetId: 'asset-image-1',
180
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
181
+ displayName: 'Homepage shot',
182
+ } }));
183
+ expect(await screen.findByText('Failed to create session.')).toBeInTheDocument();
184
+ await user.click(screen.getByRole('button', { name: /pin tool/i }));
185
+ expect(screen.getByText('No session exists for this image yet.')).toBeInTheDocument();
186
+ const createNow = screen.getByRole('button', { name: /create one now/i });
187
+ expect(createNow).toBeInTheDocument();
188
+ await user.click(createNow);
189
+ expect(await findSessionCard('Recovered review')).toBeInTheDocument();
190
+ });
191
+ it('pastes a clipboard image into a new unattached image-notes session', async () => {
192
+ const user = userEvent.setup();
193
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
194
+ ok: true,
195
+ json: async () => ({
196
+ success: true,
197
+ assetId: 'asset-pasted-1',
198
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-pasted-1.png',
199
+ displayName: 'Pasted image',
200
+ referenceLabel: 'I-24',
201
+ }),
202
+ });
203
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
204
+ configurable: true,
205
+ value: {
206
+ read: vi.fn().mockResolvedValue([
207
+ {
208
+ types: ['image/png'],
209
+ getType: vi.fn().mockResolvedValue(new Blob(['png'], { type: 'image/png' })),
210
+ },
211
+ ]),
212
+ },
213
+ });
214
+ const fileReaderReadAsDataUrl = vi
215
+ .spyOn(FileReader.prototype, 'readAsDataURL')
216
+ .mockImplementation(function mockReadAsDataUrl() {
217
+ Object.defineProperty(this, 'result', {
218
+ configurable: true,
219
+ value: 'data:image/png;base64,cG5n',
220
+ });
221
+ this.onload?.call(this, new ProgressEvent('load'));
222
+ });
223
+ fetchTaskforceApi
224
+ .mockResolvedValueOnce({
225
+ ok: true,
226
+ json: async () => ({ sessions: [] }),
227
+ })
228
+ .mockResolvedValueOnce({
229
+ ok: true,
230
+ json: async () => ({
231
+ session: buildSessionResponse({
232
+ id: 'session-pasted-1',
233
+ taskId: '',
234
+ baseImageAssetId: 'asset-pasted-1',
235
+ title: 'Pasted image review',
236
+ }),
237
+ }),
238
+ });
239
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1" }));
240
+ await user.click(screen.getByRole('button', { name: /paste image/i }));
241
+ await waitFor(() => expect(fetchSpy).toHaveBeenCalledWith('/api/taskforce/context-upload', expect.objectContaining({
242
+ method: 'POST',
243
+ credentials: 'include',
244
+ })));
245
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&imageAssetId=asset-pasted-1', expect.any(Object), ''));
246
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({
247
+ method: 'POST',
248
+ body: expect.stringContaining('"baseImageAssetId":"asset-pasted-1"'),
249
+ }), ''));
250
+ expect(await screen.findByText('Unattached Image')).toBeInTheDocument();
251
+ expect(await findSessionCard('Pasted image review')).toBeInTheDocument();
252
+ fetchSpy.mockRestore();
253
+ fileReaderReadAsDataUrl.mockRestore();
254
+ });
255
+ it('opens an image by reference from the toolbar modal', async () => {
256
+ const user = userEvent.setup();
257
+ fetchTaskforceApi
258
+ .mockResolvedValueOnce({
259
+ ok: true,
260
+ json: async () => ({
261
+ sessions: [
262
+ buildSessionResponse({
263
+ id: 'session-old-1',
264
+ title: 'Homepage shot review',
265
+ taskId: 'task-1',
266
+ baseImageAssetId: 'asset-image-1',
267
+ }),
268
+ ],
269
+ }),
270
+ })
271
+ .mockResolvedValueOnce({
272
+ ok: true,
273
+ json: async () => ({
274
+ target: {
275
+ assetId: 'asset-image-24',
276
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-24.png',
277
+ displayName: 'Reference image',
278
+ imageReferenceLabel: 'I-24',
279
+ },
280
+ }),
281
+ })
282
+ .mockResolvedValueOnce({
283
+ ok: true,
284
+ json: async () => ({ sessions: [] }),
285
+ })
286
+ .mockResolvedValueOnce({
287
+ ok: true,
288
+ json: async () => ({
289
+ session: buildSessionResponse({
290
+ id: 'session-ref-1',
291
+ taskId: '',
292
+ baseImageAssetId: 'asset-image-24',
293
+ title: 'Reference image review',
294
+ }),
295
+ }),
296
+ });
297
+ function ControlledOpenHarness() {
298
+ const [target, setTarget] = React.useState({
299
+ taskId: 'task-1',
300
+ taskReferenceLabel: 'T-101',
301
+ imageReferenceLabel: 'I-22',
302
+ assetId: 'asset-image-1',
303
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
304
+ displayName: 'Homepage shot',
305
+ });
306
+ const [sessionId, setSessionId] = React.useState(null);
307
+ return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
308
+ setTarget(nextTarget);
309
+ setSessionId(options?.sessionId ?? null);
310
+ } }));
311
+ }
312
+ render(_jsx(ControlledOpenHarness, {}));
313
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
314
+ fireEvent.load(screen.getByAltText('Homepage shot'));
315
+ await user.click(screen.getByRole('button', { name: /open image/i }));
316
+ await user.type(screen.getByLabelText(/image reference number/i), 'I-24');
317
+ await user.click(screen.getByRole('button', { name: /^open$/i }));
318
+ await waitFor(() => {
319
+ expect(screen.getByAltText('Reference image')).toBeInTheDocument();
320
+ expect(screen.queryByAltText('Homepage shot')).not.toBeInTheDocument();
321
+ });
322
+ });
323
+ it('does not revert to the previous image while opening a pasted image in controlled mode', async () => {
324
+ const user = userEvent.setup();
325
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
326
+ ok: true,
327
+ json: async () => ({
328
+ success: true,
329
+ assetId: 'asset-pasted-1',
330
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-pasted-1.png',
331
+ displayName: 'Pasted image',
332
+ referenceLabel: 'I-24',
333
+ }),
334
+ });
335
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
336
+ configurable: true,
337
+ value: {
338
+ read: vi.fn().mockResolvedValue([
339
+ {
340
+ types: ['image/png'],
341
+ getType: vi.fn().mockResolvedValue(new Blob(['png'], { type: 'image/png' })),
342
+ },
343
+ ]),
344
+ },
345
+ });
346
+ const fileReaderReadAsDataUrl = vi
347
+ .spyOn(FileReader.prototype, 'readAsDataURL')
348
+ .mockImplementation(function mockReadAsDataUrl() {
349
+ Object.defineProperty(this, 'result', {
350
+ configurable: true,
351
+ value: 'data:image/png;base64,cG5n',
352
+ });
353
+ this.onload?.call(this, new ProgressEvent('load'));
354
+ });
355
+ fetchTaskforceApi
356
+ .mockResolvedValueOnce({
357
+ ok: true,
358
+ json: async () => ({
359
+ sessions: [
360
+ buildSessionResponse({
361
+ id: 'session-old-1',
362
+ title: 'Homepage shot review',
363
+ taskId: 'task-1',
364
+ baseImageAssetId: 'asset-image-1',
365
+ }),
366
+ ],
367
+ }),
368
+ })
369
+ .mockResolvedValueOnce({
370
+ ok: true,
371
+ json: async () => ({ sessions: [] }),
372
+ })
373
+ .mockResolvedValueOnce({
374
+ ok: true,
375
+ json: async () => ({
376
+ session: buildSessionResponse({
377
+ id: 'session-pasted-1',
378
+ taskId: '',
379
+ baseImageAssetId: 'asset-pasted-1',
380
+ title: 'Pasted image review',
381
+ }),
382
+ }),
383
+ });
384
+ function ControlledPasteHarness() {
385
+ const [target, setTarget] = React.useState({
386
+ taskId: 'task-1',
387
+ taskReferenceLabel: 'T-101',
388
+ imageReferenceLabel: 'I-22',
389
+ assetId: 'asset-image-1',
390
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
391
+ displayName: 'Homepage shot',
392
+ });
393
+ const [sessionId, setSessionId] = React.useState(null);
394
+ return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
395
+ setTarget(nextTarget);
396
+ setSessionId(options?.sessionId ?? null);
397
+ } }));
398
+ }
399
+ render(_jsx(ControlledPasteHarness, {}));
400
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
401
+ fireEvent.load(screen.getByAltText('Homepage shot'));
402
+ await user.click(screen.getByRole('button', { name: /paste image/i }));
403
+ await waitFor(() => {
404
+ expect(screen.getByAltText('Pasted image')).toBeInTheDocument();
405
+ expect(screen.queryByAltText('Homepage shot')).not.toBeInTheDocument();
406
+ });
407
+ fetchSpy.mockRestore();
408
+ fileReaderReadAsDataUrl.mockRestore();
409
+ });
410
+ it('shows who created each session in the sessions list', async () => {
411
+ fetchTaskforceApi.mockResolvedValueOnce({
412
+ ok: true,
413
+ json: async () => ({
414
+ sessions: [
415
+ {
416
+ id: 'session-1',
417
+ tenantId: 'tenant-1',
418
+ workspaceId: 'workspace-1',
419
+ taskId: 'task-1',
420
+ baseImageAssetId: 'asset-image-1',
421
+ title: 'Homepage review',
422
+ globalInstruction: 'Review this screenshot',
423
+ annotations: [],
424
+ createdByActorId: 'user-1',
425
+ updatedByActorId: 'user-1',
426
+ createdAt: '2026-03-25T12:00:00.000Z',
427
+ updatedAt: '2026-03-25T12:00:00.000Z',
428
+ deletedAt: null,
429
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
430
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
431
+ },
432
+ ],
433
+ }),
434
+ });
435
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
436
+ taskId: 'task-1',
437
+ taskReferenceLabel: 'T-101',
438
+ assetId: 'asset-image-1',
439
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
440
+ displayName: 'Homepage shot',
441
+ } }));
442
+ expect(await screen.findByText('Created by Alex')).toBeInTheDocument();
443
+ });
444
+ it('shows new box and arrow markers immediately while dragging them out', async () => {
445
+ const user = userEvent.setup();
446
+ fetchTaskforceApi.mockResolvedValueOnce({
447
+ ok: true,
448
+ json: async () => ({
449
+ sessions: [
450
+ buildSessionResponse({
451
+ title: 'Homepage review',
452
+ globalInstruction: 'Review this screenshot',
453
+ }),
454
+ ],
455
+ }),
456
+ });
457
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
458
+ taskId: 'task-1',
459
+ assetId: 'asset-image-1',
460
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
461
+ displayName: 'Homepage shot',
462
+ } }));
463
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
464
+ fireEvent.load(screen.getByAltText('Homepage shot'));
465
+ const overlay = screen.getByTestId('annotated-attachment-overlay');
466
+ Object.defineProperty(overlay, 'getBoundingClientRect', {
467
+ configurable: true,
468
+ value: () => ({
469
+ left: 0,
470
+ top: 0,
471
+ width: 100,
472
+ height: 100,
473
+ right: 100,
474
+ bottom: 100,
475
+ x: 0,
476
+ y: 0,
477
+ toJSON: () => ({}),
478
+ }),
479
+ });
480
+ await user.click(screen.getByRole('button', { name: /box tool\./i }));
481
+ fireEvent.pointerDown(overlay, { clientX: 20, clientY: 25 });
482
+ expect(screen.getByRole('button', { name: 'Box' })).toBeInTheDocument();
483
+ fireEvent.pointerMove(overlay, { clientX: 45, clientY: 55 });
484
+ fireEvent.pointerUp(overlay, { clientX: 45, clientY: 55 });
485
+ await user.click(screen.getByRole('button', { name: /arrow tool\./i }));
486
+ fireEvent.pointerDown(overlay, { clientX: 60, clientY: 30 });
487
+ expect(screen.getByRole('button', { name: 'Arrow' })).toBeInTheDocument();
488
+ fireEvent.pointerMove(overlay, { clientX: 80, clientY: 60 });
489
+ fireEvent.pointerUp(overlay, { clientX: 80, clientY: 60 });
490
+ });
491
+ it('updates the selected marker color and reuses it for new markers', async () => {
492
+ const user = userEvent.setup();
493
+ fetchTaskforceApi
494
+ .mockResolvedValueOnce({
495
+ ok: true,
496
+ json: async () => ({
497
+ sessions: [
498
+ buildSessionResponse({
499
+ title: 'Homepage review',
500
+ globalInstruction: 'Review this screenshot',
501
+ annotations: [
502
+ {
503
+ id: 'annotation-1',
504
+ order: 0,
505
+ kind: 'pin',
506
+ x: 0.2,
507
+ y: 0.3,
508
+ instruction: 'Inspect this button',
509
+ markerType: 'review',
510
+ color: '#7c3aed',
511
+ },
512
+ ],
513
+ }),
514
+ ],
515
+ }),
516
+ })
517
+ .mockResolvedValueOnce({
518
+ ok: true,
519
+ json: async () => ({
520
+ session: buildSessionResponse({
521
+ title: 'Homepage review',
522
+ globalInstruction: 'Review this screenshot',
523
+ updatedAt: '2026-03-25T12:06:00.000Z',
524
+ annotations: [
525
+ {
526
+ id: 'annotation-1',
527
+ order: 0,
528
+ kind: 'pin',
529
+ x: 0.2,
530
+ y: 0.3,
531
+ instruction: 'Inspect this button',
532
+ markerType: 'review',
533
+ color: '#dc2626',
534
+ },
535
+ ],
536
+ }),
537
+ }),
538
+ })
539
+ .mockResolvedValueOnce({
540
+ ok: true,
541
+ json: async () => ({
542
+ session: buildSessionResponse({
543
+ title: 'Homepage review',
544
+ globalInstruction: 'Review this screenshot',
545
+ updatedAt: '2026-03-25T12:07:00.000Z',
546
+ annotations: [
547
+ {
548
+ id: 'annotation-1',
549
+ order: 0,
550
+ kind: 'pin',
551
+ x: 0.2,
552
+ y: 0.3,
553
+ instruction: 'Inspect this button',
554
+ markerType: 'review',
555
+ color: '#dc2626',
556
+ },
557
+ {
558
+ id: 'annotation-2',
559
+ order: 1,
560
+ kind: 'pin',
561
+ x: 0.45,
562
+ y: 0.55,
563
+ instruction: '',
564
+ markerType: 'review',
565
+ color: '#dc2626',
566
+ },
567
+ ],
568
+ }),
569
+ }),
570
+ });
571
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
572
+ taskId: 'task-1',
573
+ assetId: 'asset-image-1',
574
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
575
+ displayName: 'Homepage shot',
576
+ } }));
577
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
578
+ fireEvent.load(screen.getByAltText('Homepage shot'));
579
+ const overlay = screen.getByTestId('annotated-attachment-overlay');
580
+ Object.defineProperty(overlay, 'getBoundingClientRect', {
581
+ configurable: true,
582
+ value: () => ({
583
+ left: 0,
584
+ top: 0,
585
+ width: 100,
586
+ height: 100,
587
+ right: 100,
588
+ bottom: 100,
589
+ x: 0,
590
+ y: 0,
591
+ toJSON: () => ({}),
592
+ }),
593
+ });
594
+ await user.click(screen.getByRole('button', { name: '1' }));
595
+ await user.click(screen.getByRole('button', { name: /marker color/i }));
596
+ await user.click(screen.getByRole('button', { name: /use marker color #dc2626/i }));
597
+ await waitFor(() => {
598
+ expect(fetchTaskforceApi.mock.calls).toEqual(expect.arrayContaining([
599
+ [
600
+ '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1',
601
+ expect.objectContaining({ method: 'PATCH' }),
602
+ '',
603
+ ],
604
+ ]));
605
+ });
606
+ const recolorBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
607
+ expect(recolorBody.annotations[0]?.color).toBe('#dc2626');
608
+ await user.click(screen.getByRole('button', { name: /pin tool\./i }));
609
+ fireEvent.pointerDown(overlay, { clientX: 45, clientY: 55 });
610
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
611
+ const newMarkerBody = JSON.parse(String(fetchTaskforceApi.mock.calls[2]?.[1]?.body || '{}'));
612
+ expect(newMarkerBody.annotations).toHaveLength(2);
613
+ expect(newMarkerBody.annotations[1]?.color).toBe('#dc2626');
614
+ });
615
+ it('shows the selected session state and edits session details inline', async () => {
616
+ const user = userEvent.setup();
617
+ fetchTaskforceApi.mockResolvedValueOnce({
618
+ ok: true,
619
+ json: async () => ({
620
+ sessions: [
621
+ {
622
+ id: 'session-1',
623
+ tenantId: 'tenant-1',
624
+ workspaceId: 'workspace-1',
625
+ taskId: 'task-1',
626
+ baseImageAssetId: 'asset-image-1',
627
+ title: 'Homepage review',
628
+ globalInstruction: 'Review this screenshot',
629
+ annotations: [],
630
+ createdByActorId: 'user-1',
631
+ updatedByActorId: 'user-1',
632
+ createdAt: '2026-03-25T12:00:00.000Z',
633
+ updatedAt: '2026-03-25T12:00:00.000Z',
634
+ deletedAt: null,
635
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
636
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
637
+ },
638
+ ],
639
+ }),
640
+ });
641
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
642
+ taskId: 'task-1',
643
+ taskReferenceLabel: 'T-101',
644
+ assetId: 'asset-image-1',
645
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
646
+ displayName: 'Homepage shot',
647
+ } }));
648
+ const sessionCard = await findSessionCard('Homepage review');
649
+ expect(sessionCard).toHaveAttribute('aria-pressed', 'true');
650
+ expect(screen.queryByLabelText('Session title')).not.toBeInTheDocument();
651
+ await user.click(sessionCard);
652
+ expect(screen.getByLabelText('Session title')).toBeInTheDocument();
653
+ expect(screen.getByDisplayValue('Homepage review')).toBeInTheDocument();
654
+ expect(screen.getByDisplayValue('Review this screenshot')).toBeInTheDocument();
655
+ });
656
+ it('closes session inline editing when clicking outside the session card', async () => {
657
+ const user = userEvent.setup();
658
+ fetchTaskforceApi.mockResolvedValueOnce({
659
+ ok: true,
660
+ json: async () => ({
661
+ sessions: [
662
+ {
663
+ id: 'session-1',
664
+ tenantId: 'tenant-1',
665
+ workspaceId: 'workspace-1',
666
+ taskId: 'task-1',
667
+ baseImageAssetId: 'asset-image-1',
668
+ title: 'Homepage review',
669
+ globalInstruction: 'Review this screenshot',
670
+ annotations: [],
671
+ createdByActorId: 'user-1',
672
+ updatedByActorId: 'user-1',
673
+ createdAt: '2026-03-25T12:00:00.000Z',
674
+ updatedAt: '2026-03-25T12:00:00.000Z',
675
+ deletedAt: null,
676
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
677
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
678
+ },
679
+ ],
680
+ }),
681
+ });
682
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
683
+ taskId: 'task-1',
684
+ taskReferenceLabel: 'T-101',
685
+ assetId: 'asset-image-1',
686
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
687
+ displayName: 'Homepage shot',
688
+ } }));
689
+ await user.click(await findSessionCard('Homepage review'));
690
+ expect(screen.getByLabelText('Session title')).toBeInTheDocument();
691
+ await user.click(screen.getByRole('button', { name: /create session/i }));
692
+ expect(screen.queryByLabelText('Session title')).not.toBeInTheDocument();
693
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
694
+ });
695
+ it('shows concise titles on the marker tool rail', async () => {
696
+ fetchTaskforceApi.mockResolvedValueOnce({
697
+ ok: true,
698
+ json: async () => ({
699
+ sessions: [
700
+ {
701
+ id: 'session-1',
702
+ tenantId: 'tenant-1',
703
+ workspaceId: 'workspace-1',
704
+ taskId: 'task-1',
705
+ baseImageAssetId: 'asset-image-1',
706
+ title: 'Homepage review',
707
+ globalInstruction: 'Review this screenshot',
708
+ annotations: [],
709
+ createdByActorId: 'user-1',
710
+ updatedByActorId: 'user-1',
711
+ createdAt: '2026-03-25T12:00:00.000Z',
712
+ updatedAt: '2026-03-25T12:00:00.000Z',
713
+ deletedAt: null,
714
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
715
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
716
+ },
717
+ ],
718
+ }),
719
+ });
720
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
721
+ taskId: 'task-1',
722
+ taskReferenceLabel: 'T-101',
723
+ assetId: 'asset-image-1',
724
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
725
+ displayName: 'Homepage shot',
726
+ } }));
727
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
728
+ expect(screen.getByRole('button', { name: /pin tool\./i })).toHaveAttribute('title', 'Pin');
729
+ expect(screen.getByRole('button', { name: /box tool\./i })).toHaveAttribute('title', 'Box');
730
+ });
731
+ it('does not label the last editor as the creator when creator details are missing', async () => {
732
+ fetchTaskforceApi.mockResolvedValueOnce({
733
+ ok: true,
734
+ json: async () => ({
735
+ sessions: [
736
+ {
737
+ id: 'session-1',
738
+ tenantId: 'tenant-1',
739
+ workspaceId: 'workspace-1',
740
+ taskId: 'task-1',
741
+ baseImageAssetId: 'asset-image-1',
742
+ title: 'Homepage review',
743
+ globalInstruction: 'Review this screenshot',
744
+ annotations: [],
745
+ createdByActorId: 'user-1',
746
+ updatedByActorId: 'user-2',
747
+ createdAt: '2026-03-25T12:00:00.000Z',
748
+ updatedAt: '2026-03-25T13:00:00.000Z',
749
+ deletedAt: null,
750
+ createdByActor: null,
751
+ updatedByActor: { actorId: 'user-2', label: 'Taylor', type: 'user' },
752
+ },
753
+ ],
754
+ }),
755
+ });
756
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
757
+ taskId: 'task-1',
758
+ taskReferenceLabel: 'T-101',
759
+ assetId: 'asset-image-1',
760
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
761
+ displayName: 'Homepage shot',
762
+ } }));
763
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
764
+ expect(screen.queryByText('Created by Taylor')).not.toBeInTheDocument();
765
+ });
766
+ it('enables pan controls only after zooming in', async () => {
767
+ const user = userEvent.setup();
768
+ fetchTaskforceApi.mockResolvedValueOnce({
769
+ ok: true,
770
+ json: async () => ({
771
+ sessions: [
772
+ {
773
+ id: 'session-1',
774
+ tenantId: 'tenant-1',
775
+ workspaceId: 'workspace-1',
776
+ taskId: 'task-1',
777
+ baseImageAssetId: 'asset-image-1',
778
+ title: 'Homepage review',
779
+ globalInstruction: 'Review this screenshot',
780
+ annotations: [],
781
+ createdByActorId: 'user-1',
782
+ updatedByActorId: 'user-1',
783
+ createdAt: '2026-03-25T12:00:00.000Z',
784
+ updatedAt: '2026-03-25T12:00:00.000Z',
785
+ deletedAt: null,
786
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
787
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
788
+ },
789
+ ],
790
+ }),
791
+ });
792
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
793
+ taskId: 'task-1',
794
+ taskReferenceLabel: 'T-101',
795
+ assetId: 'asset-image-1',
796
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
797
+ displayName: 'Homepage shot',
798
+ } }));
799
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
800
+ fireEvent.load(screen.getByAltText('Homepage shot'));
801
+ const panButton = screen.getByRole('button', { name: /pan canvas/i });
802
+ expect(panButton).toBeDisabled();
803
+ expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
804
+ await user.click(screen.getByRole('button', { name: /zoom in/i }));
805
+ await waitFor(() => {
806
+ expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('125%'));
807
+ });
808
+ expect(screen.getByRole('button', { name: /pan canvas/i })).toBeEnabled();
809
+ });
810
+ it('allows zooming out below 100 percent for large screenshots', async () => {
811
+ const user = userEvent.setup();
812
+ fetchTaskforceApi.mockResolvedValueOnce({
813
+ ok: true,
814
+ json: async () => ({
815
+ sessions: [
816
+ {
817
+ id: 'session-1',
818
+ tenantId: 'tenant-1',
819
+ workspaceId: 'workspace-1',
820
+ taskId: 'task-1',
821
+ baseImageAssetId: 'asset-image-1',
822
+ title: 'Homepage review',
823
+ globalInstruction: 'Review this screenshot',
824
+ annotations: [],
825
+ createdByActorId: 'user-1',
826
+ updatedByActorId: 'user-1',
827
+ createdAt: '2026-03-25T12:00:00.000Z',
828
+ updatedAt: '2026-03-25T12:00:00.000Z',
829
+ deletedAt: null,
830
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
831
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
832
+ },
833
+ ],
834
+ }),
835
+ });
836
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
837
+ taskId: 'task-1',
838
+ taskReferenceLabel: 'T-101',
839
+ assetId: 'asset-image-1',
840
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
841
+ displayName: 'Large screenshot',
842
+ } }));
843
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
844
+ fireEvent.load(screen.getByAltText('Large screenshot'));
845
+ expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
846
+ await user.click(screen.getByRole('button', { name: /zoom out/i }));
847
+ await waitFor(() => {
848
+ expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('75%'));
849
+ });
850
+ expect(screen.getByRole('button', { name: /pan canvas/i })).toBeDisabled();
851
+ });
852
+ it('keeps the image at natural size at 100 percent even in a narrow canvas viewport', async () => {
853
+ fetchTaskforceApi.mockResolvedValueOnce({
854
+ ok: true,
855
+ json: async () => ({
856
+ sessions: [
857
+ {
858
+ id: 'session-1',
859
+ tenantId: 'tenant-1',
860
+ workspaceId: 'workspace-1',
861
+ taskId: 'task-1',
862
+ baseImageAssetId: 'asset-image-1',
863
+ title: 'Homepage review',
864
+ globalInstruction: 'Review this screenshot',
865
+ annotations: [],
866
+ createdByActorId: 'user-1',
867
+ updatedByActorId: 'user-1',
868
+ createdAt: '2026-03-25T12:00:00.000Z',
869
+ updatedAt: '2026-03-25T12:00:00.000Z',
870
+ deletedAt: null,
871
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
872
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
873
+ },
874
+ ],
875
+ }),
876
+ });
877
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
878
+ taskId: 'task-1',
879
+ taskReferenceLabel: 'T-101',
880
+ assetId: 'asset-image-1',
881
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
882
+ displayName: 'Wide screenshot',
883
+ } }));
884
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
885
+ const image = screen.getByAltText('Wide screenshot');
886
+ Object.defineProperty(image, 'naturalWidth', { configurable: true, value: 1200 });
887
+ Object.defineProperty(image, 'naturalHeight', { configurable: true, value: 800 });
888
+ const canvasScroller = image.parentElement?.parentElement?.parentElement;
889
+ expect(canvasScroller).not.toBeNull();
890
+ Object.defineProperty(canvasScroller, 'clientWidth', { configurable: true, value: 400 });
891
+ fireEvent.load(image);
892
+ await waitFor(() => {
893
+ expect(image.parentElement).toHaveStyle({
894
+ width: '1200px',
895
+ height: '800px',
896
+ });
897
+ });
898
+ expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
899
+ expect(screen.getByRole('button', { name: /pan canvas/i })).toBeEnabled();
900
+ });
901
+ it('does not reload sessions when rerendered with the same annotated target', async () => {
902
+ fetchTaskforceApi.mockResolvedValueOnce({
903
+ ok: true,
904
+ json: async () => ({
905
+ sessions: [
906
+ {
907
+ id: 'session-1',
908
+ tenantId: 'tenant-1',
909
+ workspaceId: 'workspace-1',
910
+ taskId: 'task-1',
911
+ baseImageAssetId: 'asset-image-1',
912
+ title: 'Homepage review',
913
+ globalInstruction: 'Review this screenshot',
914
+ annotations: [],
915
+ createdByActorId: 'user-1',
916
+ updatedByActorId: 'user-1',
917
+ createdAt: '2026-03-25T12:00:00.000Z',
918
+ updatedAt: '2026-03-25T12:00:00.000Z',
919
+ deletedAt: null,
920
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
921
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
922
+ },
923
+ ],
924
+ }),
925
+ });
926
+ const requestedTarget = {
927
+ taskId: 'task-1',
928
+ taskReferenceLabel: 'T-101',
929
+ assetId: 'asset-image-1',
930
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
931
+ displayName: 'Homepage shot',
932
+ };
933
+ const { rerender } = render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, onContextChange: () => { } }));
934
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
935
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
936
+ rerender(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, onContextChange: () => { } }));
937
+ await waitFor(() => {
938
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
939
+ });
940
+ });
941
+ it('reloads sessions when the same annotated target is explicitly reopened', async () => {
942
+ fetchTaskforceApi
943
+ .mockResolvedValueOnce({
944
+ ok: true,
945
+ json: async () => ({
946
+ sessions: [
947
+ {
948
+ id: 'session-1',
949
+ tenantId: 'tenant-1',
950
+ workspaceId: 'workspace-1',
951
+ taskId: 'task-1',
952
+ baseImageAssetId: 'asset-image-1',
953
+ title: 'Older review',
954
+ globalInstruction: 'Original session list',
955
+ annotations: [],
956
+ createdByActorId: 'user-1',
957
+ updatedByActorId: 'user-1',
958
+ createdAt: '2026-03-25T12:00:00.000Z',
959
+ updatedAt: '2026-03-25T12:00:00.000Z',
960
+ deletedAt: null,
961
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
962
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
963
+ },
964
+ ],
965
+ }),
966
+ })
967
+ .mockResolvedValueOnce({
968
+ ok: true,
969
+ json: async () => ({
970
+ sessions: [
971
+ {
972
+ id: 'session-2',
973
+ tenantId: 'tenant-1',
974
+ workspaceId: 'workspace-1',
975
+ taskId: 'task-1',
976
+ baseImageAssetId: 'asset-image-1',
977
+ title: 'Newest review',
978
+ globalInstruction: 'Freshly loaded session list',
979
+ annotations: [],
980
+ createdByActorId: 'user-2',
981
+ updatedByActorId: 'user-2',
982
+ createdAt: '2026-04-03T22:00:00.000Z',
983
+ updatedAt: '2026-04-03T22:00:00.000Z',
984
+ deletedAt: null,
985
+ createdByActor: { actorId: 'user-2', label: 'Jordan', type: 'user' },
986
+ updatedByActor: { actorId: 'user-2', label: 'Jordan', type: 'user' },
987
+ },
988
+ ],
989
+ }),
990
+ });
991
+ const requestedTarget = {
992
+ taskId: 'task-1',
993
+ taskReferenceLabel: 'T-101',
994
+ assetId: 'asset-image-1',
995
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
996
+ displayName: 'Homepage shot',
997
+ };
998
+ const { rerender } = render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, requestedOpenVersion: 1, onContextChange: () => { } }));
999
+ expect(await findSessionCard('Older review')).toBeInTheDocument();
1000
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
1001
+ rerender(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, requestedOpenVersion: 2, onContextChange: () => { } }));
1002
+ expect(await findSessionCard('Newest review')).toBeInTheDocument();
1003
+ await waitFor(() => {
1004
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1005
+ });
1006
+ });
1007
+ it('reloads sessions when the document becomes visible and the editor is not dirty', async () => {
1008
+ fetchTaskforceApi
1009
+ .mockResolvedValueOnce({
1010
+ ok: true,
1011
+ json: async () => ({
1012
+ sessions: [
1013
+ {
1014
+ id: 'session-1',
1015
+ tenantId: 'tenant-1',
1016
+ workspaceId: 'workspace-1',
1017
+ taskId: 'task-1',
1018
+ baseImageAssetId: 'asset-image-1',
1019
+ title: 'Homepage review',
1020
+ globalInstruction: 'Review this screenshot',
1021
+ annotations: [],
1022
+ createdByActorId: 'user-1',
1023
+ updatedByActorId: 'user-1',
1024
+ createdAt: '2026-03-25T12:00:00.000Z',
1025
+ updatedAt: '2026-03-25T12:00:00.000Z',
1026
+ deletedAt: null,
1027
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1028
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1029
+ },
1030
+ ],
1031
+ }),
1032
+ })
1033
+ .mockResolvedValueOnce({
1034
+ ok: true,
1035
+ json: async () => ({
1036
+ sessions: [
1037
+ {
1038
+ id: 'session-2',
1039
+ tenantId: 'tenant-1',
1040
+ workspaceId: 'workspace-1',
1041
+ taskId: 'task-1',
1042
+ baseImageAssetId: 'asset-image-1',
1043
+ title: 'Synced on visibility',
1044
+ globalInstruction: 'Server-authoritative',
1045
+ annotations: [],
1046
+ createdByActorId: 'user-1',
1047
+ updatedByActorId: 'user-1',
1048
+ createdAt: '2026-03-25T12:05:00.000Z',
1049
+ updatedAt: '2026-03-25T12:06:00.000Z',
1050
+ deletedAt: null,
1051
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1052
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1053
+ },
1054
+ ],
1055
+ }),
1056
+ });
1057
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1058
+ taskId: 'task-1',
1059
+ taskReferenceLabel: 'T-101',
1060
+ assetId: 'asset-image-1',
1061
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1062
+ displayName: 'Homepage shot',
1063
+ } }));
1064
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1065
+ Object.defineProperty(document, 'visibilityState', {
1066
+ configurable: true,
1067
+ value: 'visible',
1068
+ });
1069
+ fireEvent(document, new Event('visibilitychange'));
1070
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&taskId=task-1&imageAssetId=asset-image-1', expect.objectContaining({
1071
+ credentials: 'include',
1072
+ headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1073
+ }), ''));
1074
+ expect(await findSessionCard('Synced on visibility')).toBeInTheDocument();
1075
+ });
1076
+ it('uses the provided task reference resolver when the launch target lacks a readable label', async () => {
1077
+ const user = userEvent.setup();
1078
+ const writeText = vi.fn().mockResolvedValue(undefined);
1079
+ Object.defineProperty(navigator, 'clipboard', {
1080
+ configurable: true,
1081
+ value: { writeText },
1082
+ });
1083
+ mockAutoCreateFirstSession();
1084
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1085
+ taskId: 'task-1',
1086
+ assetId: 'asset-image-1',
1087
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1088
+ displayName: 'Homepage shot',
1089
+ }, resolveTaskReferenceLabel: () => 'T-101' }));
1090
+ await findSessionCard('Homepage shot review');
1091
+ expect(screen.getByText('T-101')).toBeInTheDocument();
1092
+ await user.click(screen.getByRole('button', { name: /copy task id/i }));
1093
+ expect(writeText).toHaveBeenCalledWith('T-101');
1094
+ });
1095
+ it('prefers the resolved T-XXX reference when the incoming label is just the raw task id', async () => {
1096
+ const user = userEvent.setup();
1097
+ const writeText = vi.fn().mockResolvedValue(undefined);
1098
+ Object.defineProperty(navigator, 'clipboard', {
1099
+ configurable: true,
1100
+ value: { writeText },
1101
+ });
1102
+ mockAutoCreateFirstSession();
1103
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1104
+ taskId: 'task-1',
1105
+ taskReferenceLabel: 'task-1',
1106
+ assetId: 'asset-image-1',
1107
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1108
+ displayName: 'Homepage shot',
1109
+ }, resolveTaskReferenceLabel: () => 'T-101' }));
1110
+ await findSessionCard('Homepage shot review');
1111
+ expect(screen.getByText('T-101')).toBeInTheDocument();
1112
+ await user.click(screen.getByRole('button', { name: /copy task id/i }));
1113
+ expect(writeText).toHaveBeenCalledWith('T-101');
1114
+ });
1115
+ it('uses the provided image reference resolver when the launch target lacks a readable image label', async () => {
1116
+ const user = userEvent.setup();
1117
+ const writeText = vi.fn().mockResolvedValue(undefined);
1118
+ Object.defineProperty(navigator, 'clipboard', {
1119
+ configurable: true,
1120
+ value: { writeText },
1121
+ });
1122
+ mockAutoCreateFirstSession();
1123
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1124
+ assetId: 'asset-image-1',
1125
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1126
+ displayName: 'Standalone shot',
1127
+ }, resolveImageReferenceLabel: () => 'I-22' }));
1128
+ await findSessionCard('Homepage shot review');
1129
+ expect(screen.getByText('I-22')).toBeInTheDocument();
1130
+ await user.click(screen.getByRole('button', { name: /copy image reference/i }));
1131
+ expect(writeText).toHaveBeenCalledWith('I-22');
1132
+ expect(screen.getByRole('button', { name: /copied image reference/i })).toBeInTheDocument();
1133
+ });
1134
+ it('deletes the selected marker when the user presses Delete', async () => {
1135
+ const user = userEvent.setup();
1136
+ fetchTaskforceApi.mockResolvedValueOnce({
1137
+ ok: true,
1138
+ json: async () => ({
1139
+ sessions: [
1140
+ {
1141
+ id: 'session-1',
1142
+ tenantId: 'tenant-1',
1143
+ workspaceId: 'workspace-1',
1144
+ taskId: 'task-1',
1145
+ baseImageAssetId: 'asset-image-1',
1146
+ title: 'Homepage review',
1147
+ globalInstruction: 'Review this screenshot',
1148
+ annotations: [
1149
+ {
1150
+ id: 'annotation-1',
1151
+ order: 0,
1152
+ kind: 'pin',
1153
+ x: 0.2,
1154
+ y: 0.3,
1155
+ instruction: 'Inspect this button',
1156
+ markerType: 'question',
1157
+ color: '#0f766e',
1158
+ },
1159
+ ],
1160
+ createdByActorId: 'user-1',
1161
+ updatedByActorId: 'user-1',
1162
+ createdAt: '2026-03-25T12:00:00.000Z',
1163
+ updatedAt: '2026-03-25T12:00:00.000Z',
1164
+ deletedAt: null,
1165
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1166
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1167
+ },
1168
+ ],
1169
+ }),
1170
+ });
1171
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1172
+ taskId: 'task-1',
1173
+ taskReferenceLabel: 'T-101',
1174
+ assetId: 'asset-image-1',
1175
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1176
+ displayName: 'Homepage shot',
1177
+ } }));
1178
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1179
+ await user.click(screen.getByRole('button', { name: 'Pin' }));
1180
+ await user.keyboard('{Delete}');
1181
+ await waitFor(() => {
1182
+ expect(screen.queryByRole('button', { name: 'Pin' })).not.toBeInTheDocument();
1183
+ });
1184
+ expect(screen.getByText('Add a marker on the image to begin.')).toBeInTheDocument();
1185
+ });
1186
+ it('does not delete the selected marker when Delete is pressed while typing in a field', async () => {
1187
+ const user = userEvent.setup();
1188
+ fetchTaskforceApi.mockResolvedValueOnce({
1189
+ ok: true,
1190
+ json: async () => ({
1191
+ sessions: [
1192
+ {
1193
+ id: 'session-1',
1194
+ tenantId: 'tenant-1',
1195
+ workspaceId: 'workspace-1',
1196
+ taskId: 'task-1',
1197
+ baseImageAssetId: 'asset-image-1',
1198
+ title: 'Homepage review',
1199
+ globalInstruction: 'Review this screenshot',
1200
+ annotations: [
1201
+ {
1202
+ id: 'annotation-1',
1203
+ order: 0,
1204
+ kind: 'pin',
1205
+ x: 0.2,
1206
+ y: 0.3,
1207
+ instruction: 'Inspect this button',
1208
+ markerType: 'question',
1209
+ color: '#0f766e',
1210
+ },
1211
+ ],
1212
+ createdByActorId: 'user-1',
1213
+ updatedByActorId: 'user-1',
1214
+ createdAt: '2026-03-25T12:00:00.000Z',
1215
+ updatedAt: '2026-03-25T12:00:00.000Z',
1216
+ deletedAt: null,
1217
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1218
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1219
+ },
1220
+ ],
1221
+ }),
1222
+ });
1223
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1224
+ taskId: 'task-1',
1225
+ taskReferenceLabel: 'T-101',
1226
+ assetId: 'asset-image-1',
1227
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1228
+ displayName: 'Homepage shot',
1229
+ } }));
1230
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1231
+ await user.click(screen.getByRole('button', { name: 'Pin' }));
1232
+ const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
1233
+ instructionField.focus();
1234
+ fireEvent.keyDown(instructionField, { key: 'Delete' });
1235
+ expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument();
1236
+ });
1237
+ it('duplicates and deletes sessions within the annotated attachment module shell', async () => {
1238
+ const user = userEvent.setup();
1239
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
1240
+ fetchTaskforceApi
1241
+ .mockResolvedValueOnce({
1242
+ ok: true,
1243
+ json: async () => ({
1244
+ sessions: [
1245
+ {
1246
+ id: 'session-1',
1247
+ tenantId: 'tenant-1',
1248
+ workspaceId: 'workspace-1',
1249
+ taskId: 'task-1',
1250
+ baseImageAssetId: 'asset-image-1',
1251
+ title: 'Homepage review',
1252
+ globalInstruction: 'Review this screenshot',
1253
+ annotations: [
1254
+ {
1255
+ id: 'annotation-1',
1256
+ order: 0,
1257
+ kind: 'pin',
1258
+ x: 0.2,
1259
+ y: 0.3,
1260
+ instruction: 'Inspect this button',
1261
+ markerType: 'question',
1262
+ color: '#0f766e',
1263
+ },
1264
+ ],
1265
+ createdByActorId: 'user-1',
1266
+ updatedByActorId: 'user-1',
1267
+ createdAt: '2026-03-25T12:00:00.000Z',
1268
+ updatedAt: '2026-03-25T12:00:00.000Z',
1269
+ deletedAt: null,
1270
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1271
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1272
+ },
1273
+ ],
1274
+ }),
1275
+ })
1276
+ .mockResolvedValueOnce({
1277
+ ok: true,
1278
+ json: async () => ({
1279
+ success: true,
1280
+ session: {
1281
+ id: 'session-2',
1282
+ tenantId: 'tenant-1',
1283
+ workspaceId: 'workspace-1',
1284
+ taskId: 'task-1',
1285
+ baseImageAssetId: 'asset-image-1',
1286
+ title: 'Homepage review copy',
1287
+ globalInstruction: 'Review this screenshot',
1288
+ annotations: [
1289
+ {
1290
+ id: 'annotation-copy-1',
1291
+ order: 0,
1292
+ kind: 'pin',
1293
+ x: 0.2,
1294
+ y: 0.3,
1295
+ instruction: 'Inspect this button',
1296
+ markerType: 'question',
1297
+ color: '#0f766e',
1298
+ },
1299
+ ],
1300
+ createdByActorId: 'user-1',
1301
+ updatedByActorId: 'user-1',
1302
+ createdAt: '2026-03-25T12:05:00.000Z',
1303
+ updatedAt: '2026-03-25T12:05:00.000Z',
1304
+ deletedAt: null,
1305
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1306
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1307
+ },
1308
+ }),
1309
+ })
1310
+ .mockResolvedValueOnce({
1311
+ ok: true,
1312
+ json: async () => ({
1313
+ sessions: [
1314
+ {
1315
+ id: 'session-2',
1316
+ tenantId: 'tenant-1',
1317
+ workspaceId: 'workspace-1',
1318
+ taskId: 'task-1',
1319
+ baseImageAssetId: 'asset-image-1',
1320
+ title: 'Homepage review copy',
1321
+ globalInstruction: 'Review this screenshot',
1322
+ annotations: [
1323
+ {
1324
+ id: 'annotation-copy-1',
1325
+ order: 0,
1326
+ kind: 'pin',
1327
+ x: 0.2,
1328
+ y: 0.3,
1329
+ instruction: 'Inspect this button',
1330
+ markerType: 'question',
1331
+ color: '#0f766e',
1332
+ },
1333
+ ],
1334
+ createdByActorId: 'user-1',
1335
+ updatedByActorId: 'user-1',
1336
+ createdAt: '2026-03-25T12:05:00.000Z',
1337
+ updatedAt: '2026-03-25T12:05:00.000Z',
1338
+ deletedAt: null,
1339
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1340
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1341
+ },
1342
+ {
1343
+ id: 'session-1',
1344
+ tenantId: 'tenant-1',
1345
+ workspaceId: 'workspace-1',
1346
+ taskId: 'task-1',
1347
+ baseImageAssetId: 'asset-image-1',
1348
+ title: 'Homepage review',
1349
+ globalInstruction: 'Review this screenshot',
1350
+ annotations: [
1351
+ {
1352
+ id: 'annotation-1',
1353
+ order: 0,
1354
+ kind: 'pin',
1355
+ x: 0.2,
1356
+ y: 0.3,
1357
+ instruction: 'Inspect this button',
1358
+ markerType: 'question',
1359
+ color: '#0f766e',
1360
+ },
1361
+ ],
1362
+ createdByActorId: 'user-1',
1363
+ updatedByActorId: 'user-1',
1364
+ createdAt: '2026-03-25T12:00:00.000Z',
1365
+ updatedAt: '2026-03-25T12:00:00.000Z',
1366
+ deletedAt: null,
1367
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1368
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1369
+ },
1370
+ ],
1371
+ }),
1372
+ })
1373
+ .mockResolvedValueOnce({
1374
+ ok: true,
1375
+ json: async () => ({
1376
+ session: {
1377
+ id: 'session-2',
1378
+ tenantId: 'tenant-1',
1379
+ workspaceId: 'workspace-1',
1380
+ taskId: 'task-1',
1381
+ baseImageAssetId: 'asset-image-1',
1382
+ title: 'Homepage review copy',
1383
+ globalInstruction: 'Review this screenshot',
1384
+ annotations: [
1385
+ {
1386
+ id: 'annotation-copy-1',
1387
+ order: 0,
1388
+ kind: 'pin',
1389
+ x: 0.2,
1390
+ y: 0.3,
1391
+ instruction: 'Inspect this button',
1392
+ markerType: 'question',
1393
+ color: '#0f766e',
1394
+ },
1395
+ ],
1396
+ createdByActorId: 'user-1',
1397
+ updatedByActorId: 'user-1',
1398
+ createdAt: '2026-03-25T12:04:00.000Z',
1399
+ updatedAt: '2026-03-25T12:04:00.000Z',
1400
+ deletedAt: null,
1401
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1402
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1403
+ },
1404
+ }),
1405
+ })
1406
+ .mockResolvedValueOnce({
1407
+ ok: true,
1408
+ json: async () => ({ success: true }),
1409
+ });
1410
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1411
+ taskId: 'task-1',
1412
+ taskReferenceLabel: 'T-101',
1413
+ assetId: 'asset-image-1',
1414
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1415
+ displayName: 'Homepage shot',
1416
+ } }));
1417
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&taskId=task-1&imageAssetId=asset-image-1', expect.objectContaining({
1418
+ credentials: 'include',
1419
+ headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1420
+ }), ''));
1421
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1422
+ await user.click(screen.getByRole('button', { name: /duplicate session/i }));
1423
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({ method: 'POST' }), ''));
1424
+ const duplicateBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1425
+ expect(duplicateBody.title).toBe('Homepage review copy');
1426
+ expect(duplicateBody.annotations).toHaveLength(1);
1427
+ expect(duplicateBody.annotations[0]?.instruction).toBe('Inspect this button');
1428
+ expect(await findSessionCard('Homepage review copy')).toBeInTheDocument();
1429
+ await user.click(screen.getByRole('button', { name: /delete session/i }));
1430
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-2?workspaceId=workspace-1', expect.objectContaining({ method: 'DELETE' }), ''));
1431
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(3);
1432
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1433
+ expect(confirmSpy).toHaveBeenCalledWith('Delete the annotated attachment session "Homepage review copy"?');
1434
+ });
1435
+ it('auto-creates the first session and uses the created session immediately on entry', async () => {
1436
+ fetchTaskforceApi
1437
+ .mockResolvedValueOnce({
1438
+ ok: true,
1439
+ json: async () => ({
1440
+ sessions: [],
1441
+ }),
1442
+ })
1443
+ .mockResolvedValueOnce({
1444
+ ok: true,
1445
+ json: async () => ({
1446
+ success: true,
1447
+ session: {
1448
+ id: 'session-2',
1449
+ tenantId: 'tenant-1',
1450
+ workspaceId: 'workspace-1',
1451
+ taskId: 'task-1',
1452
+ baseImageAssetId: 'asset-image-1',
1453
+ title: 'Homepage shot notes',
1454
+ globalInstruction: '',
1455
+ annotations: [],
1456
+ createdByActorId: 'user-1',
1457
+ updatedByActorId: 'user-1',
1458
+ createdAt: '2026-03-25T12:05:00.000Z',
1459
+ updatedAt: '2026-03-25T12:05:00.000Z',
1460
+ deletedAt: null,
1461
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1462
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1463
+ },
1464
+ }),
1465
+ });
1466
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1467
+ taskId: 'task-1',
1468
+ taskReferenceLabel: 'T-101',
1469
+ assetId: 'asset-image-1',
1470
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1471
+ displayName: 'Homepage shot',
1472
+ } }));
1473
+ expect(await findSessionCard('Homepage shot notes')).toBeInTheDocument();
1474
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({ method: 'POST' }), ''));
1475
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1476
+ expect(screen.getByText('Created by Alex')).toBeInTheDocument();
1477
+ });
1478
+ it('reorders markers and saves the updated annotation order', async () => {
1479
+ const user = userEvent.setup();
1480
+ fetchTaskforceApi
1481
+ .mockResolvedValueOnce({
1482
+ ok: true,
1483
+ json: async () => ({
1484
+ sessions: [
1485
+ {
1486
+ id: 'session-1',
1487
+ tenantId: 'tenant-1',
1488
+ workspaceId: 'workspace-1',
1489
+ taskId: 'task-1',
1490
+ baseImageAssetId: 'asset-image-1',
1491
+ title: 'Homepage review',
1492
+ globalInstruction: 'Review this screenshot',
1493
+ annotations: [
1494
+ {
1495
+ id: 'annotation-1',
1496
+ order: 0,
1497
+ kind: 'pin',
1498
+ x: 0.2,
1499
+ y: 0.3,
1500
+ instruction: 'Inspect this button',
1501
+ markerType: 'question',
1502
+ color: '#0f766e',
1503
+ },
1504
+ {
1505
+ id: 'annotation-2',
1506
+ order: 1,
1507
+ kind: 'box',
1508
+ x: 0.4,
1509
+ y: 0.2,
1510
+ width: 0.2,
1511
+ height: 0.2,
1512
+ instruction: 'Review this card',
1513
+ markerType: 'review',
1514
+ color: '#7c3aed',
1515
+ },
1516
+ ],
1517
+ createdByActorId: 'user-1',
1518
+ updatedByActorId: 'user-1',
1519
+ createdAt: '2026-03-25T12:00:00.000Z',
1520
+ updatedAt: '2026-03-25T12:00:00.000Z',
1521
+ deletedAt: null,
1522
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1523
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1524
+ },
1525
+ ],
1526
+ }),
1527
+ })
1528
+ .mockResolvedValueOnce({
1529
+ ok: true,
1530
+ json: async () => ({
1531
+ success: true,
1532
+ session: {
1533
+ id: 'session-1',
1534
+ tenantId: 'tenant-1',
1535
+ workspaceId: 'workspace-1',
1536
+ taskId: 'task-1',
1537
+ baseImageAssetId: 'asset-image-1',
1538
+ title: 'Homepage review',
1539
+ globalInstruction: 'Review this screenshot',
1540
+ annotations: [
1541
+ {
1542
+ id: 'annotation-2',
1543
+ order: 0,
1544
+ kind: 'box',
1545
+ x: 0.4,
1546
+ y: 0.2,
1547
+ width: 0.2,
1548
+ height: 0.2,
1549
+ instruction: 'Review this card',
1550
+ markerType: 'review',
1551
+ color: '#7c3aed',
1552
+ },
1553
+ {
1554
+ id: 'annotation-1',
1555
+ order: 1,
1556
+ kind: 'pin',
1557
+ x: 0.2,
1558
+ y: 0.3,
1559
+ instruction: 'Inspect this button',
1560
+ markerType: 'question',
1561
+ color: '#0f766e',
1562
+ },
1563
+ ],
1564
+ createdByActorId: 'user-1',
1565
+ updatedByActorId: 'user-1',
1566
+ createdAt: '2026-03-25T12:00:00.000Z',
1567
+ updatedAt: '2026-03-25T12:10:00.000Z',
1568
+ deletedAt: null,
1569
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1570
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1571
+ },
1572
+ }),
1573
+ });
1574
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1575
+ taskId: 'task-1',
1576
+ taskReferenceLabel: 'T-101',
1577
+ assetId: 'asset-image-1',
1578
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1579
+ displayName: 'Homepage shot',
1580
+ } }));
1581
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1582
+ expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument();
1583
+ await user.click(screen.getByRole('button', { name: /move marker down/i }));
1584
+ expect(screen.getByRole('button', { name: 'Box' })).toBeInTheDocument();
1585
+ await user.click(screen.getByRole('button', { name: /save session/i }));
1586
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1587
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1588
+ expect(saveBody.annotations.map((annotation) => annotation.kind)).toEqual(['box', 'pin']);
1589
+ expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1590
+ expect(await screen.findByRole('button', { name: 'Box' })).toBeInTheDocument();
1591
+ });
1592
+ it('loads an AI payload preview for the selected session', async () => {
1593
+ const user = userEvent.setup();
1594
+ fetchTaskforceApi
1595
+ .mockResolvedValueOnce({
1596
+ ok: true,
1597
+ json: async () => ({
1598
+ sessions: [
1599
+ {
1600
+ id: 'session-1',
1601
+ tenantId: 'tenant-1',
1602
+ workspaceId: 'workspace-1',
1603
+ taskId: 'task-1',
1604
+ baseImageAssetId: 'asset-image-1',
1605
+ title: 'Homepage review',
1606
+ globalInstruction: 'Review this screenshot',
1607
+ annotations: [
1608
+ {
1609
+ id: 'annotation-1',
1610
+ order: 0,
1611
+ kind: 'pin',
1612
+ x: 0.2,
1613
+ y: 0.3,
1614
+ instruction: 'Inspect this button',
1615
+ markerType: 'question',
1616
+ color: '#0f766e',
1617
+ },
1618
+ ],
1619
+ createdByActorId: 'user-1',
1620
+ updatedByActorId: 'user-1',
1621
+ createdAt: '2026-03-25T12:00:00.000Z',
1622
+ updatedAt: '2026-03-25T12:00:00.000Z',
1623
+ deletedAt: null,
1624
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1625
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1626
+ },
1627
+ ],
1628
+ }),
1629
+ })
1630
+ .mockResolvedValueOnce({
1631
+ ok: true,
1632
+ json: async () => ({
1633
+ payload: {
1634
+ taskId: 'task-1',
1635
+ workspaceId: 'workspace-1',
1636
+ sessionId: 'session-1',
1637
+ title: 'Homepage review',
1638
+ globalInstruction: 'Review this screenshot',
1639
+ image: {
1640
+ assetId: 'asset-image-1',
1641
+ storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
1642
+ apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1643
+ mimeType: 'image/png',
1644
+ originalFilename: 'homepage.png',
1645
+ logicalName: 'homepage',
1646
+ displayName: 'Homepage shot',
1647
+ sizeBytes: 1024,
1648
+ updatedAt: '2026-03-25T12:00:00.000Z',
1649
+ },
1650
+ annotations: [
1651
+ {
1652
+ id: 'annotation-1',
1653
+ order: 0,
1654
+ kind: 'pin',
1655
+ x: 0.2,
1656
+ y: 0.3,
1657
+ instruction: 'Inspect this button',
1658
+ markerType: 'question',
1659
+ color: '#0f766e',
1660
+ cropHint: {
1661
+ x: 0.11,
1662
+ y: 0.21,
1663
+ width: 0.18,
1664
+ height: 0.18,
1665
+ },
1666
+ },
1667
+ ],
1668
+ },
1669
+ }),
1670
+ });
1671
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1672
+ taskId: 'task-1',
1673
+ taskReferenceLabel: 'T-101',
1674
+ assetId: 'asset-image-1',
1675
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1676
+ displayName: 'Homepage shot',
1677
+ } }));
1678
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1679
+ await user.click(screen.getByRole('button', { name: /preview payload/i }));
1680
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1/payload?workspaceId=workspace-1', expect.objectContaining({
1681
+ credentials: 'include',
1682
+ headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1683
+ }), ''));
1684
+ expect(await screen.findByText(/"sessionId": "session-1"/)).toBeInTheDocument();
1685
+ expect(screen.getByText(/"cropHint"/)).toBeInTheDocument();
1686
+ });
1687
+ it('updates marker geometry fields and saves the edited coordinates', async () => {
1688
+ const user = userEvent.setup();
1689
+ fetchTaskforceApi
1690
+ .mockResolvedValueOnce({
1691
+ ok: true,
1692
+ json: async () => ({
1693
+ sessions: [
1694
+ {
1695
+ id: 'session-1',
1696
+ tenantId: 'tenant-1',
1697
+ workspaceId: 'workspace-1',
1698
+ taskId: 'task-1',
1699
+ baseImageAssetId: 'asset-image-1',
1700
+ title: 'Homepage review',
1701
+ globalInstruction: 'Review this screenshot',
1702
+ annotations: [
1703
+ {
1704
+ id: 'annotation-1',
1705
+ order: 0,
1706
+ kind: 'box',
1707
+ x: 0.4,
1708
+ y: 0.2,
1709
+ width: 0.2,
1710
+ height: 0.2,
1711
+ instruction: 'Review this card',
1712
+ markerType: 'review',
1713
+ color: '#7c3aed',
1714
+ },
1715
+ ],
1716
+ createdByActorId: 'user-1',
1717
+ updatedByActorId: 'user-1',
1718
+ createdAt: '2026-03-25T12:00:00.000Z',
1719
+ updatedAt: '2026-03-25T12:00:00.000Z',
1720
+ deletedAt: null,
1721
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1722
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1723
+ },
1724
+ ],
1725
+ }),
1726
+ })
1727
+ .mockResolvedValueOnce({
1728
+ ok: true,
1729
+ json: async () => ({
1730
+ success: true,
1731
+ session: {
1732
+ id: 'session-1',
1733
+ tenantId: 'tenant-1',
1734
+ workspaceId: 'workspace-1',
1735
+ taskId: 'task-1',
1736
+ baseImageAssetId: 'asset-image-1',
1737
+ title: 'Homepage review',
1738
+ globalInstruction: 'Review this screenshot',
1739
+ annotations: [
1740
+ {
1741
+ id: 'annotation-1',
1742
+ order: 0,
1743
+ kind: 'box',
1744
+ x: 0.4,
1745
+ y: 0.2,
1746
+ width: 0.35,
1747
+ height: 0.25,
1748
+ instruction: 'Review this card',
1749
+ markerType: 'review',
1750
+ color: '#7c3aed',
1751
+ },
1752
+ ],
1753
+ createdByActorId: 'user-1',
1754
+ updatedByActorId: 'user-1',
1755
+ createdAt: '2026-03-25T12:00:00.000Z',
1756
+ updatedAt: '2026-03-25T12:15:00.000Z',
1757
+ deletedAt: null,
1758
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1759
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1760
+ },
1761
+ }),
1762
+ });
1763
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1764
+ taskId: 'task-1',
1765
+ taskReferenceLabel: 'T-101',
1766
+ assetId: 'asset-image-1',
1767
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1768
+ displayName: 'Homepage shot',
1769
+ } }));
1770
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1771
+ const widthInput = screen.getByRole('spinbutton', { name: /width percent/i });
1772
+ const heightInput = screen.getByRole('spinbutton', { name: /height percent/i });
1773
+ fireEvent.change(widthInput, { target: { value: '35' } });
1774
+ fireEvent.change(heightInput, { target: { value: '25' } });
1775
+ await user.click(screen.getByRole('button', { name: /save session/i }));
1776
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1777
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1778
+ expect(saveBody.annotations[0]?.width).toBe(0.35);
1779
+ expect(saveBody.annotations[0]?.height).toBe(0.25);
1780
+ });
1781
+ it('selects and drags an arrow marker from the canvas hit area', async () => {
1782
+ const user = userEvent.setup();
1783
+ fetchTaskforceApi
1784
+ .mockResolvedValueOnce({
1785
+ ok: true,
1786
+ json: async () => ({
1787
+ sessions: [
1788
+ {
1789
+ id: 'session-1',
1790
+ tenantId: 'tenant-1',
1791
+ workspaceId: 'workspace-1',
1792
+ taskId: 'task-1',
1793
+ baseImageAssetId: 'asset-image-1',
1794
+ title: 'Homepage review',
1795
+ globalInstruction: 'Review this screenshot',
1796
+ annotations: [
1797
+ {
1798
+ id: 'annotation-pin',
1799
+ order: 0,
1800
+ kind: 'pin',
1801
+ x: 0.15,
1802
+ y: 0.2,
1803
+ instruction: 'Inspect this button',
1804
+ markerType: 'question',
1805
+ color: '#0f766e',
1806
+ },
1807
+ {
1808
+ id: 'annotation-arrow',
1809
+ order: 1,
1810
+ kind: 'arrow',
1811
+ x: 0.2,
1812
+ y: 0.3,
1813
+ x2: 0.4,
1814
+ y2: 0.5,
1815
+ instruction: 'Follow this flow',
1816
+ markerType: 'change',
1817
+ color: '#2563eb',
1818
+ },
1819
+ ],
1820
+ createdByActorId: 'user-1',
1821
+ updatedByActorId: 'user-1',
1822
+ createdAt: '2026-03-25T12:00:00.000Z',
1823
+ updatedAt: '2026-03-25T12:00:00.000Z',
1824
+ deletedAt: null,
1825
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1826
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1827
+ },
1828
+ ],
1829
+ }),
1830
+ })
1831
+ .mockResolvedValueOnce({
1832
+ ok: true,
1833
+ json: async () => ({
1834
+ success: true,
1835
+ session: {
1836
+ id: 'session-1',
1837
+ tenantId: 'tenant-1',
1838
+ workspaceId: 'workspace-1',
1839
+ taskId: 'task-1',
1840
+ baseImageAssetId: 'asset-image-1',
1841
+ title: 'Homepage review',
1842
+ globalInstruction: 'Review this screenshot',
1843
+ annotations: [
1844
+ {
1845
+ id: 'annotation-pin',
1846
+ order: 0,
1847
+ kind: 'pin',
1848
+ x: 0.15,
1849
+ y: 0.2,
1850
+ instruction: 'Inspect this button',
1851
+ markerType: 'question',
1852
+ color: '#0f766e',
1853
+ },
1854
+ {
1855
+ id: 'annotation-arrow',
1856
+ order: 1,
1857
+ kind: 'arrow',
1858
+ x: 0.3,
1859
+ y: 0.4,
1860
+ x2: 0.5,
1861
+ y2: 0.6,
1862
+ instruction: 'Follow this flow',
1863
+ markerType: 'change',
1864
+ color: '#2563eb',
1865
+ },
1866
+ ],
1867
+ createdByActorId: 'user-1',
1868
+ updatedByActorId: 'user-1',
1869
+ createdAt: '2026-03-25T12:00:00.000Z',
1870
+ updatedAt: '2026-03-25T12:18:00.000Z',
1871
+ deletedAt: null,
1872
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1873
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1874
+ },
1875
+ }),
1876
+ });
1877
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1878
+ taskId: 'task-1',
1879
+ assetId: 'asset-image-1',
1880
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1881
+ displayName: 'Homepage shot',
1882
+ } }));
1883
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1884
+ fireEvent.load(screen.getByAltText('Homepage shot'));
1885
+ const overlay = screen.getByTestId('annotated-attachment-overlay');
1886
+ Object.defineProperty(overlay, 'getBoundingClientRect', {
1887
+ configurable: true,
1888
+ value: () => ({
1889
+ left: 0,
1890
+ top: 0,
1891
+ width: 100,
1892
+ height: 100,
1893
+ right: 100,
1894
+ bottom: 100,
1895
+ x: 0,
1896
+ y: 0,
1897
+ toJSON: () => ({}),
1898
+ }),
1899
+ });
1900
+ const arrowHitArea = screen.getByTestId('annotated-arrow-hit-annotation-arrow');
1901
+ fireEvent.pointerDown(arrowHitArea, { clientX: 20, clientY: 30 });
1902
+ fireEvent.pointerMove(overlay, { clientX: 30, clientY: 40 });
1903
+ fireEvent.pointerUp(overlay, { clientX: 30, clientY: 40 });
1904
+ expect(screen.getByDisplayValue('Follow this flow')).toBeInTheDocument();
1905
+ await user.click(screen.getByRole('button', { name: /save session/i }));
1906
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1907
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1908
+ expect(saveBody.annotations[1]?.x).toBeCloseTo(0.3, 6);
1909
+ expect(saveBody.annotations[1]?.y).toBeCloseTo(0.4, 6);
1910
+ expect(saveBody.annotations[1]?.x2).toBeCloseTo(0.5, 6);
1911
+ expect(saveBody.annotations[1]?.y2).toBeCloseTo(0.6, 6);
1912
+ });
1913
+ it('resizes a selected box marker from the canvas handle', async () => {
1914
+ const user = userEvent.setup();
1915
+ fetchTaskforceApi
1916
+ .mockResolvedValueOnce({
1917
+ ok: true,
1918
+ json: async () => ({
1919
+ sessions: [
1920
+ {
1921
+ id: 'session-1',
1922
+ tenantId: 'tenant-1',
1923
+ workspaceId: 'workspace-1',
1924
+ taskId: 'task-1',
1925
+ baseImageAssetId: 'asset-image-1',
1926
+ title: 'Homepage review',
1927
+ globalInstruction: 'Review this screenshot',
1928
+ annotations: [
1929
+ {
1930
+ id: 'annotation-box',
1931
+ order: 0,
1932
+ kind: 'box',
1933
+ x: 0.2,
1934
+ y: 0.25,
1935
+ width: 0.2,
1936
+ height: 0.2,
1937
+ instruction: 'Resize this region',
1938
+ markerType: 'review',
1939
+ color: '#7c3aed',
1940
+ },
1941
+ ],
1942
+ createdByActorId: 'user-1',
1943
+ updatedByActorId: 'user-1',
1944
+ createdAt: '2026-03-25T12:00:00.000Z',
1945
+ updatedAt: '2026-03-25T12:00:00.000Z',
1946
+ deletedAt: null,
1947
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1948
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1949
+ },
1950
+ ],
1951
+ }),
1952
+ })
1953
+ .mockResolvedValueOnce({
1954
+ ok: true,
1955
+ json: async () => ({
1956
+ success: true,
1957
+ session: {
1958
+ id: 'session-1',
1959
+ tenantId: 'tenant-1',
1960
+ workspaceId: 'workspace-1',
1961
+ taskId: 'task-1',
1962
+ baseImageAssetId: 'asset-image-1',
1963
+ title: 'Homepage review',
1964
+ globalInstruction: 'Review this screenshot',
1965
+ annotations: [
1966
+ {
1967
+ id: 'annotation-box',
1968
+ order: 0,
1969
+ kind: 'box',
1970
+ x: 0.2,
1971
+ y: 0.25,
1972
+ width: 0.3,
1973
+ height: 0.3,
1974
+ instruction: 'Resize this region',
1975
+ markerType: 'review',
1976
+ color: '#7c3aed',
1977
+ },
1978
+ ],
1979
+ createdByActorId: 'user-1',
1980
+ updatedByActorId: 'user-1',
1981
+ createdAt: '2026-03-25T12:00:00.000Z',
1982
+ updatedAt: '2026-03-25T12:21:00.000Z',
1983
+ deletedAt: null,
1984
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1985
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1986
+ },
1987
+ }),
1988
+ });
1989
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1990
+ taskId: 'task-1',
1991
+ assetId: 'asset-image-1',
1992
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1993
+ displayName: 'Homepage shot',
1994
+ } }));
1995
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1996
+ fireEvent.load(screen.getByAltText('Homepage shot'));
1997
+ const overlay = screen.getByTestId('annotated-attachment-overlay');
1998
+ Object.defineProperty(overlay, 'getBoundingClientRect', {
1999
+ configurable: true,
2000
+ value: () => ({
2001
+ left: 0,
2002
+ top: 0,
2003
+ width: 100,
2004
+ height: 100,
2005
+ right: 100,
2006
+ bottom: 100,
2007
+ x: 0,
2008
+ y: 0,
2009
+ toJSON: () => ({}),
2010
+ }),
2011
+ });
2012
+ await user.click(screen.getByRole('button', { name: 'Box' }));
2013
+ const resizeHandle = screen.getByRole('button', { name: /resize box marker/i });
2014
+ fireEvent.pointerDown(resizeHandle, { clientX: 40, clientY: 45 });
2015
+ fireEvent.pointerMove(overlay, { clientX: 50, clientY: 55 });
2016
+ fireEvent.pointerUp(overlay, { clientX: 50, clientY: 55 });
2017
+ await user.click(screen.getByRole('button', { name: /save session/i }));
2018
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2019
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2020
+ expect(saveBody.annotations[0]?.width).toBeCloseTo(0.3, 6);
2021
+ expect(saveBody.annotations[0]?.height).toBeCloseTo(0.3, 6);
2022
+ });
2023
+ it('moves the selected arrow end handle on the canvas', async () => {
2024
+ const user = userEvent.setup();
2025
+ fetchTaskforceApi
2026
+ .mockResolvedValueOnce({
2027
+ ok: true,
2028
+ json: async () => ({
2029
+ sessions: [
2030
+ {
2031
+ id: 'session-1',
2032
+ tenantId: 'tenant-1',
2033
+ workspaceId: 'workspace-1',
2034
+ taskId: 'task-1',
2035
+ baseImageAssetId: 'asset-image-1',
2036
+ title: 'Homepage review',
2037
+ globalInstruction: 'Review this screenshot',
2038
+ annotations: [
2039
+ {
2040
+ id: 'annotation-arrow',
2041
+ order: 0,
2042
+ kind: 'arrow',
2043
+ x: 0.2,
2044
+ y: 0.3,
2045
+ x2: 0.4,
2046
+ y2: 0.5,
2047
+ instruction: 'Move this endpoint',
2048
+ markerType: 'change',
2049
+ color: '#2563eb',
2050
+ },
2051
+ ],
2052
+ createdByActorId: 'user-1',
2053
+ updatedByActorId: 'user-1',
2054
+ createdAt: '2026-03-25T12:00:00.000Z',
2055
+ updatedAt: '2026-03-25T12:00:00.000Z',
2056
+ deletedAt: null,
2057
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2058
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2059
+ },
2060
+ ],
2061
+ }),
2062
+ })
2063
+ .mockResolvedValueOnce({
2064
+ ok: true,
2065
+ json: async () => ({
2066
+ success: true,
2067
+ session: {
2068
+ id: 'session-1',
2069
+ tenantId: 'tenant-1',
2070
+ workspaceId: 'workspace-1',
2071
+ taskId: 'task-1',
2072
+ baseImageAssetId: 'asset-image-1',
2073
+ title: 'Homepage review',
2074
+ globalInstruction: 'Review this screenshot',
2075
+ annotations: [
2076
+ {
2077
+ id: 'annotation-arrow',
2078
+ order: 0,
2079
+ kind: 'arrow',
2080
+ x: 0.2,
2081
+ y: 0.3,
2082
+ x2: 0.55,
2083
+ y2: 0.65,
2084
+ instruction: 'Move this endpoint',
2085
+ markerType: 'change',
2086
+ color: '#2563eb',
2087
+ },
2088
+ ],
2089
+ createdByActorId: 'user-1',
2090
+ updatedByActorId: 'user-1',
2091
+ createdAt: '2026-03-25T12:00:00.000Z',
2092
+ updatedAt: '2026-03-25T12:24:00.000Z',
2093
+ deletedAt: null,
2094
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2095
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2096
+ },
2097
+ }),
2098
+ });
2099
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2100
+ taskId: 'task-1',
2101
+ assetId: 'asset-image-1',
2102
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2103
+ displayName: 'Homepage shot',
2104
+ } }));
2105
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2106
+ fireEvent.load(screen.getByAltText('Homepage shot'));
2107
+ const overlay = screen.getByTestId('annotated-attachment-overlay');
2108
+ Object.defineProperty(overlay, 'getBoundingClientRect', {
2109
+ configurable: true,
2110
+ value: () => ({
2111
+ left: 0,
2112
+ top: 0,
2113
+ width: 100,
2114
+ height: 100,
2115
+ right: 100,
2116
+ bottom: 100,
2117
+ x: 0,
2118
+ y: 0,
2119
+ toJSON: () => ({}),
2120
+ }),
2121
+ });
2122
+ await user.click(screen.getByRole('button', { name: 'Arrow' }));
2123
+ const endHandle = screen.getByRole('button', { name: /move arrow tip/i });
2124
+ fireEvent.pointerDown(endHandle, { clientX: 40, clientY: 50 });
2125
+ fireEvent.pointerMove(overlay, { clientX: 55, clientY: 65 });
2126
+ fireEvent.pointerUp(overlay, { clientX: 55, clientY: 65 });
2127
+ await user.click(screen.getByRole('button', { name: /save session/i }));
2128
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2129
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2130
+ expect(saveBody.annotations[0]?.x2).toBeCloseTo(0.55, 6);
2131
+ expect(saveBody.annotations[0]?.y2).toBeCloseTo(0.65, 6);
2132
+ });
2133
+ it('copies payload JSON and an AI brief from the payload preview', async () => {
2134
+ const user = userEvent.setup();
2135
+ const writeText = vi.fn().mockResolvedValue(undefined);
2136
+ Object.defineProperty(navigator, 'clipboard', {
2137
+ configurable: true,
2138
+ value: { writeText },
2139
+ });
2140
+ fetchTaskforceApi
2141
+ .mockResolvedValueOnce({
2142
+ ok: true,
2143
+ json: async () => ({
2144
+ sessions: [
2145
+ {
2146
+ id: 'session-1',
2147
+ tenantId: 'tenant-1',
2148
+ workspaceId: 'workspace-1',
2149
+ taskId: 'task-1',
2150
+ baseImageAssetId: 'asset-image-1',
2151
+ title: 'Homepage review',
2152
+ globalInstruction: 'Review this screenshot',
2153
+ annotations: [
2154
+ {
2155
+ id: 'annotation-1',
2156
+ order: 0,
2157
+ kind: 'pin',
2158
+ x: 0.2,
2159
+ y: 0.3,
2160
+ instruction: 'Inspect this button',
2161
+ markerType: 'question',
2162
+ color: '#0f766e',
2163
+ },
2164
+ ],
2165
+ createdByActorId: 'user-1',
2166
+ updatedByActorId: 'user-1',
2167
+ createdAt: '2026-03-25T12:00:00.000Z',
2168
+ updatedAt: '2026-03-25T12:00:00.000Z',
2169
+ deletedAt: null,
2170
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2171
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2172
+ },
2173
+ ],
2174
+ }),
2175
+ })
2176
+ .mockResolvedValueOnce({
2177
+ ok: true,
2178
+ json: async () => ({
2179
+ payload: {
2180
+ taskId: 'task-1',
2181
+ workspaceId: 'workspace-1',
2182
+ sessionId: 'session-1',
2183
+ title: 'Homepage review',
2184
+ globalInstruction: 'Review this screenshot',
2185
+ image: {
2186
+ assetId: 'asset-image-1',
2187
+ storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
2188
+ apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2189
+ mimeType: 'image/png',
2190
+ originalFilename: 'homepage.png',
2191
+ logicalName: 'homepage',
2192
+ displayName: 'Homepage shot',
2193
+ sizeBytes: 1024,
2194
+ updatedAt: '2026-03-25T12:00:00.000Z',
2195
+ },
2196
+ annotations: [
2197
+ {
2198
+ id: 'annotation-1',
2199
+ order: 0,
2200
+ kind: 'pin',
2201
+ x: 0.2,
2202
+ y: 0.3,
2203
+ instruction: 'Inspect this button',
2204
+ markerType: 'question',
2205
+ color: '#0f766e',
2206
+ cropHint: {
2207
+ x: 0.11,
2208
+ y: 0.21,
2209
+ width: 0.18,
2210
+ height: 0.18,
2211
+ },
2212
+ },
2213
+ ],
2214
+ },
2215
+ }),
2216
+ });
2217
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2218
+ taskId: 'task-1',
2219
+ assetId: 'asset-image-1',
2220
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2221
+ displayName: 'Homepage shot',
2222
+ } }));
2223
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2224
+ await user.click(screen.getByRole('button', { name: /preview payload/i }));
2225
+ expect(await screen.findByText(/"sessionId": "session-1"/)).toBeInTheDocument();
2226
+ await user.click(screen.getByRole('button', { name: /copy json/i }));
2227
+ expect(writeText).toHaveBeenNthCalledWith(1, expect.stringContaining('"sessionId": "session-1"'));
2228
+ await user.click(screen.getByRole('button', { name: /copy ai brief/i }));
2229
+ expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('Annotated attachment: Homepage review'));
2230
+ expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('1. pin (question)'));
2231
+ expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('Instruction: Inspect this button'));
2232
+ });
2233
+ it('shows a back to task button when launched from a task and calls back with the task id', async () => {
2234
+ const user = userEvent.setup();
2235
+ const onBackToTask = vi.fn();
2236
+ mockAutoCreateFirstSession();
2237
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2238
+ taskId: 'task-1',
2239
+ assetId: 'asset-image-1',
2240
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2241
+ displayName: 'Homepage shot',
2242
+ }, onBackToTask: onBackToTask }));
2243
+ await findSessionCard('Homepage shot review');
2244
+ await user.click(screen.getByRole('button', { name: /back to task/i }));
2245
+ expect(onBackToTask).toHaveBeenCalledWith('task-1');
2246
+ });
2247
+ it('shows an unattached image label when the requested target is not linked to a task', async () => {
2248
+ fetchTaskforceApi.mockResolvedValueOnce({
2249
+ ok: true,
2250
+ json: async () => ({
2251
+ sessions: [],
2252
+ }),
2253
+ });
2254
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2255
+ taskId: '',
2256
+ assetId: 'asset-image-1',
2257
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2258
+ displayName: 'Loose image',
2259
+ } }));
2260
+ await screen.findByText('No sessions yet');
2261
+ expect(screen.getByText('Unattached Image')).toBeInTheDocument();
2262
+ expect(screen.queryByRole('button', { name: /copy task id/i })).not.toBeInTheDocument();
2263
+ });
2264
+ it('does not repeat the image name in the sessions header', async () => {
2265
+ mockAutoCreateFirstSession({ title: 'Image Notes review' });
2266
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2267
+ taskId: 'task-1',
2268
+ taskReferenceLabel: 'T-101',
2269
+ assetId: 'asset-image-1',
2270
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2271
+ displayName: 'Image Notes',
2272
+ } }));
2273
+ await findSessionCard('Image Notes review');
2274
+ expect(screen.getByText('Image Notes')).toBeInTheDocument();
2275
+ expect(screen.queryByText('Create and switch between annotation sessions.')).not.toBeInTheDocument();
2276
+ });
2277
+ it('restores a requested saved session and reports the active context', async () => {
2278
+ const onContextChange = vi.fn();
2279
+ fetchTaskforceApi.mockResolvedValueOnce({
2280
+ ok: true,
2281
+ json: async () => ({
2282
+ sessions: [
2283
+ {
2284
+ id: 'session-a',
2285
+ tenantId: 'tenant-1',
2286
+ workspaceId: 'workspace-1',
2287
+ taskId: 'task-1',
2288
+ baseImageAssetId: 'asset-image-1',
2289
+ title: 'First session',
2290
+ globalInstruction: null,
2291
+ annotations: [],
2292
+ createdByActorId: 'user-1',
2293
+ updatedByActorId: 'user-1',
2294
+ createdAt: '2026-03-25T12:00:00.000Z',
2295
+ updatedAt: '2026-03-25T12:00:00.000Z',
2296
+ deletedAt: null,
2297
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2298
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2299
+ },
2300
+ {
2301
+ id: 'session-b',
2302
+ tenantId: 'tenant-1',
2303
+ workspaceId: 'workspace-1',
2304
+ taskId: 'task-1',
2305
+ baseImageAssetId: 'asset-image-1',
2306
+ title: 'Restored session',
2307
+ globalInstruction: 'Resume here',
2308
+ annotations: [],
2309
+ createdByActorId: 'user-1',
2310
+ updatedByActorId: 'user-1',
2311
+ createdAt: '2026-03-25T12:05:00.000Z',
2312
+ updatedAt: '2026-03-25T12:05:00.000Z',
2313
+ deletedAt: null,
2314
+ createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2315
+ updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2316
+ }
2317
+ ],
2318
+ }),
2319
+ });
2320
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2321
+ taskId: 'task-1',
2322
+ assetId: 'asset-image-1',
2323
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2324
+ displayName: 'Homepage shot',
2325
+ }, requestedSessionId: "session-b", onContextChange: onContextChange }));
2326
+ expect(await findSessionCard('Restored session')).toBeInTheDocument();
2327
+ await waitFor(() => {
2328
+ expect(onContextChange).toHaveBeenLastCalledWith(expect.objectContaining({
2329
+ target: expect.objectContaining({
2330
+ taskId: 'task-1',
2331
+ assetId: 'asset-image-1',
2332
+ }),
2333
+ sessionId: 'session-b',
2334
+ }));
2335
+ });
2336
+ });
2337
+ it('warns when a requested saved session cannot be restored', async () => {
2338
+ fetchTaskforceApi
2339
+ .mockResolvedValueOnce({
2340
+ ok: true,
2341
+ json: async () => ({
2342
+ sessions: [],
2343
+ }),
2344
+ })
2345
+ .mockResolvedValueOnce({
2346
+ ok: true,
2347
+ json: async () => ({
2348
+ session: buildSessionResponse({
2349
+ id: 'session-new',
2350
+ taskId: '',
2351
+ baseImageAssetId: 'asset-image-1',
2352
+ title: 'Recovered session',
2353
+ }),
2354
+ }),
2355
+ });
2356
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2357
+ taskId: '',
2358
+ assetId: 'asset-image-1',
2359
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2360
+ displayName: 'Homepage shot',
2361
+ }, requestedSessionId: "missing-session" }));
2362
+ expect(await screen.findByText('The previously selected annotation session could not be restored.')).toBeInTheDocument();
2363
+ expect(await findSessionCard('Recovered session')).toBeInTheDocument();
2364
+ });
2365
+ it('copies the originating task id from the annotator header', async () => {
2366
+ const user = userEvent.setup();
2367
+ const writeText = vi.fn().mockResolvedValue(undefined);
2368
+ Object.defineProperty(navigator, 'clipboard', {
2369
+ configurable: true,
2370
+ value: { writeText },
2371
+ });
2372
+ mockAutoCreateFirstSession();
2373
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2374
+ taskId: 'task-1',
2375
+ taskReferenceLabel: 'T-101',
2376
+ assetId: 'asset-image-1',
2377
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2378
+ displayName: 'Homepage shot',
2379
+ } }));
2380
+ await findSessionCard('Homepage shot review');
2381
+ await user.click(screen.getByRole('button', { name: /copy task id/i }));
2382
+ expect(writeText).toHaveBeenCalledWith('T-101');
2383
+ expect(screen.getByRole('button', { name: /copied task id/i })).toBeInTheDocument();
2384
+ });
2385
+ it('copies the originating image reference from the annotator header', async () => {
2386
+ const user = userEvent.setup();
2387
+ const writeText = vi.fn().mockResolvedValue(undefined);
2388
+ Object.defineProperty(navigator, 'clipboard', {
2389
+ configurable: true,
2390
+ value: { writeText },
2391
+ });
2392
+ mockAutoCreateFirstSession();
2393
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2394
+ taskId: 'task-1',
2395
+ taskReferenceLabel: 'T-101',
2396
+ imageReferenceLabel: 'I-22',
2397
+ assetId: 'asset-image-1',
2398
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2399
+ displayName: 'Homepage shot',
2400
+ } }));
2401
+ await findSessionCard('Homepage shot review');
2402
+ await user.click(screen.getByRole('button', { name: /copy image reference/i }));
2403
+ expect(writeText).toHaveBeenCalledWith('I-22');
2404
+ expect(screen.getByRole('button', { name: /copied image reference/i })).toBeInTheDocument();
2405
+ });
2406
+ it('auto-saves marker instruction edits after the debounce window', async () => {
2407
+ fetchTaskforceApi
2408
+ .mockResolvedValueOnce({
2409
+ ok: true,
2410
+ json: async () => ({
2411
+ sessions: [
2412
+ buildSessionResponse({
2413
+ globalInstruction: 'Review this screenshot',
2414
+ annotations: [
2415
+ {
2416
+ id: 'annotation-1',
2417
+ order: 0,
2418
+ kind: 'pin',
2419
+ x: 0.2,
2420
+ y: 0.3,
2421
+ instruction: 'Inspect this button',
2422
+ markerType: 'review',
2423
+ color: '#0f766e',
2424
+ },
2425
+ ],
2426
+ }),
2427
+ ],
2428
+ }),
2429
+ })
2430
+ .mockResolvedValueOnce({
2431
+ ok: true,
2432
+ json: async () => ({
2433
+ session: buildSessionResponse({
2434
+ globalInstruction: 'Review this screenshot',
2435
+ updatedAt: '2026-03-25T12:05:00.000Z',
2436
+ annotations: [
2437
+ {
2438
+ id: 'annotation-1',
2439
+ order: 0,
2440
+ kind: 'pin',
2441
+ x: 0.2,
2442
+ y: 0.3,
2443
+ instruction: 'Inspect this header CTA',
2444
+ markerType: 'review',
2445
+ color: '#0f766e',
2446
+ },
2447
+ ],
2448
+ }),
2449
+ }),
2450
+ });
2451
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2452
+ taskId: 'task-1',
2453
+ taskReferenceLabel: 'T-101',
2454
+ assetId: 'asset-image-1',
2455
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2456
+ displayName: 'Homepage shot',
2457
+ } }));
2458
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2459
+ const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2460
+ fireEvent.change(instructionField, { target: { value: 'Inspect this header CTA' } });
2461
+ expect(screen.getByText('Saving…')).toBeInTheDocument();
2462
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2463
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2464
+ expect(saveBody.annotations[0]?.instruction).toBe('Inspect this header CTA');
2465
+ await waitFor(() => expect(screen.getByText('Saved')).toBeInTheDocument());
2466
+ });
2467
+ it('flushes marker instruction edits immediately on blur', async () => {
2468
+ fetchTaskforceApi
2469
+ .mockResolvedValueOnce({
2470
+ ok: true,
2471
+ json: async () => ({
2472
+ sessions: [
2473
+ buildSessionResponse({
2474
+ globalInstruction: 'Review this screenshot',
2475
+ annotations: [
2476
+ {
2477
+ id: 'annotation-1',
2478
+ order: 0,
2479
+ kind: 'pin',
2480
+ x: 0.2,
2481
+ y: 0.3,
2482
+ instruction: 'Inspect this button',
2483
+ markerType: 'review',
2484
+ color: '#0f766e',
2485
+ },
2486
+ ],
2487
+ }),
2488
+ ],
2489
+ }),
2490
+ })
2491
+ .mockResolvedValueOnce({
2492
+ ok: true,
2493
+ json: async () => ({
2494
+ session: buildSessionResponse({
2495
+ globalInstruction: 'Review this screenshot',
2496
+ updatedAt: '2026-03-25T12:05:00.000Z',
2497
+ annotations: [
2498
+ {
2499
+ id: 'annotation-1',
2500
+ order: 0,
2501
+ kind: 'pin',
2502
+ x: 0.2,
2503
+ y: 0.3,
2504
+ instruction: 'Inspect this header CTA',
2505
+ markerType: 'review',
2506
+ color: '#0f766e',
2507
+ },
2508
+ ],
2509
+ }),
2510
+ }),
2511
+ });
2512
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2513
+ taskId: 'task-1',
2514
+ taskReferenceLabel: 'T-101',
2515
+ assetId: 'asset-image-1',
2516
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2517
+ displayName: 'Homepage shot',
2518
+ } }));
2519
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2520
+ const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2521
+ fireEvent.change(instructionField, { target: { value: 'Inspect this header CTA' } });
2522
+ fireEvent.blur(instructionField);
2523
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2524
+ const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2525
+ expect(saveBody.annotations[0]?.instruction).toBe('Inspect this header CTA');
2526
+ });
2527
+ it('clears the previous session state immediately when switching to a new target', async () => {
2528
+ const oldSession = buildSessionResponse({
2529
+ id: 'session-old',
2530
+ title: 'Old session',
2531
+ annotations: [
2532
+ {
2533
+ id: 'annotation-1',
2534
+ order: 0,
2535
+ kind: 'pin',
2536
+ x: 0.2,
2537
+ y: 0.3,
2538
+ instruction: 'Old note',
2539
+ markerType: 'review',
2540
+ color: '#0f766e',
2541
+ },
2542
+ ],
2543
+ });
2544
+ fetchTaskforceApi
2545
+ .mockResolvedValueOnce({
2546
+ ok: true,
2547
+ json: async () => ({ sessions: [oldSession] }),
2548
+ })
2549
+ .mockResolvedValueOnce({
2550
+ ok: true,
2551
+ json: async () => ({ sessions: [] }),
2552
+ })
2553
+ .mockResolvedValueOnce({
2554
+ ok: true,
2555
+ json: async () => ({
2556
+ session: buildSessionResponse({
2557
+ id: 'session-new',
2558
+ taskId: '',
2559
+ baseImageAssetId: 'asset-image-2',
2560
+ title: 'New session',
2561
+ annotations: [],
2562
+ }),
2563
+ }),
2564
+ });
2565
+ function ControlledTargetHarness() {
2566
+ const [target, setTarget] = React.useState({
2567
+ taskId: 'task-1',
2568
+ assetId: 'asset-image-1',
2569
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2570
+ displayName: 'Old image',
2571
+ });
2572
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => setTarget({
2573
+ taskId: '',
2574
+ assetId: 'asset-image-2',
2575
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-2.png',
2576
+ displayName: 'New image',
2577
+ }), children: "Switch target" }), _jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target })] }));
2578
+ }
2579
+ const user = userEvent.setup();
2580
+ render(_jsx(ControlledTargetHarness, {}));
2581
+ expect(await findSessionCard('Old session')).toBeInTheDocument();
2582
+ expect(screen.getByText('Old note')).toBeInTheDocument();
2583
+ await user.click(screen.getByRole('button', { name: /switch target/i }));
2584
+ await waitFor(() => {
2585
+ expect(screen.queryByText('Old session')).not.toBeInTheDocument();
2586
+ expect(screen.queryByText('Old note')).not.toBeInTheDocument();
2587
+ expect(screen.getByText('New session')).toBeInTheDocument();
2588
+ });
2589
+ });
2590
+ it('flushes pending edits before loading the payload preview', async () => {
2591
+ const user = userEvent.setup();
2592
+ fetchTaskforceApi
2593
+ .mockResolvedValueOnce({
2594
+ ok: true,
2595
+ json: async () => ({
2596
+ sessions: [
2597
+ buildSessionResponse({
2598
+ globalInstruction: 'Review this screenshot',
2599
+ annotations: [
2600
+ {
2601
+ id: 'annotation-1',
2602
+ order: 0,
2603
+ kind: 'pin',
2604
+ x: 0.2,
2605
+ y: 0.3,
2606
+ instruction: 'Inspect this button',
2607
+ markerType: 'review',
2608
+ color: '#0f766e',
2609
+ },
2610
+ ],
2611
+ }),
2612
+ ],
2613
+ }),
2614
+ })
2615
+ .mockResolvedValueOnce({
2616
+ ok: true,
2617
+ json: async () => ({
2618
+ session: buildSessionResponse({
2619
+ globalInstruction: 'Review this screenshot',
2620
+ updatedAt: '2026-03-25T12:05:00.000Z',
2621
+ annotations: [
2622
+ {
2623
+ id: 'annotation-1',
2624
+ order: 0,
2625
+ kind: 'pin',
2626
+ x: 0.2,
2627
+ y: 0.3,
2628
+ instruction: 'Inspect the signup button',
2629
+ markerType: 'review',
2630
+ color: '#0f766e',
2631
+ },
2632
+ ],
2633
+ }),
2634
+ }),
2635
+ })
2636
+ .mockResolvedValueOnce({
2637
+ ok: true,
2638
+ json: async () => ({
2639
+ payload: {
2640
+ taskId: 'task-1',
2641
+ workspaceId: 'workspace-1',
2642
+ sessionId: 'session-1',
2643
+ title: 'Homepage shot review',
2644
+ globalInstruction: 'Review this screenshot',
2645
+ image: {
2646
+ assetId: 'asset-image-1',
2647
+ storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
2648
+ apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2649
+ mimeType: 'image/png',
2650
+ originalFilename: 'homepage.png',
2651
+ logicalName: 'homepage',
2652
+ displayName: 'Homepage shot',
2653
+ sizeBytes: 1024,
2654
+ updatedAt: '2026-03-25T12:00:00.000Z',
2655
+ },
2656
+ annotations: [
2657
+ {
2658
+ id: 'annotation-1',
2659
+ order: 0,
2660
+ kind: 'pin',
2661
+ x: 0.2,
2662
+ y: 0.3,
2663
+ instruction: 'Inspect the signup button',
2664
+ markerType: 'review',
2665
+ color: '#0f766e',
2666
+ },
2667
+ ],
2668
+ },
2669
+ }),
2670
+ });
2671
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2672
+ taskId: 'task-1',
2673
+ taskReferenceLabel: 'T-101',
2674
+ assetId: 'asset-image-1',
2675
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2676
+ displayName: 'Homepage shot',
2677
+ } }));
2678
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2679
+ fireEvent.change(screen.getByRole('textbox', { name: /^marker instruction$/i }), {
2680
+ target: { value: 'Inspect the signup button' },
2681
+ });
2682
+ await user.click(screen.getByRole('button', { name: /preview payload/i }));
2683
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2684
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1/payload?workspaceId=workspace-1', expect.objectContaining({ credentials: 'include' }), ''));
2685
+ expect(await screen.findByText(/"Inspect the signup button"/)).toBeInTheDocument();
2686
+ });
2687
+ it('flushes the current draft before switching sessions', async () => {
2688
+ const user = userEvent.setup();
2689
+ fetchTaskforceApi
2690
+ .mockResolvedValueOnce({
2691
+ ok: true,
2692
+ json: async () => ({
2693
+ sessions: [
2694
+ buildSessionResponse({
2695
+ id: 'session-1',
2696
+ title: 'Homepage review',
2697
+ globalInstruction: 'Review the homepage',
2698
+ annotations: [
2699
+ {
2700
+ id: 'annotation-1',
2701
+ order: 0,
2702
+ kind: 'pin',
2703
+ x: 0.2,
2704
+ y: 0.3,
2705
+ instruction: 'Inspect this button',
2706
+ markerType: 'review',
2707
+ color: '#0f766e',
2708
+ },
2709
+ ],
2710
+ }),
2711
+ buildSessionResponse({
2712
+ id: 'session-2',
2713
+ title: 'Secondary review',
2714
+ globalInstruction: 'Check spacing',
2715
+ annotations: [],
2716
+ updatedAt: '2026-03-25T12:08:00.000Z',
2717
+ }),
2718
+ ],
2719
+ }),
2720
+ })
2721
+ .mockResolvedValueOnce({
2722
+ ok: true,
2723
+ json: async () => ({
2724
+ session: buildSessionResponse({
2725
+ id: 'session-1',
2726
+ title: 'Homepage review',
2727
+ globalInstruction: 'Review the homepage',
2728
+ updatedAt: '2026-03-25T12:06:00.000Z',
2729
+ annotations: [
2730
+ {
2731
+ id: 'annotation-1',
2732
+ order: 0,
2733
+ kind: 'pin',
2734
+ x: 0.2,
2735
+ y: 0.3,
2736
+ instruction: 'Inspect the hero CTA',
2737
+ markerType: 'review',
2738
+ color: '#0f766e',
2739
+ },
2740
+ ],
2741
+ }),
2742
+ }),
2743
+ });
2744
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2745
+ taskId: 'task-1',
2746
+ taskReferenceLabel: 'T-101',
2747
+ assetId: 'asset-image-1',
2748
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2749
+ displayName: 'Homepage shot',
2750
+ } }));
2751
+ expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2752
+ fireEvent.change(screen.getByRole('textbox', { name: /^marker instruction$/i }), {
2753
+ target: { value: 'Inspect the hero CTA before switching sessions' },
2754
+ });
2755
+ await user.click(screen.getByRole('button', { name: /secondary review/i }));
2756
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2757
+ expect(await screen.findByText('0 in this session')).toBeInTheDocument();
2758
+ expect(screen.getByText('Add a marker on the image to begin.')).toBeInTheDocument();
2759
+ });
2760
+ it('keeps local edits visible after a save failure and allows manual retry', async () => {
2761
+ const user = userEvent.setup();
2762
+ fetchTaskforceApi
2763
+ .mockResolvedValueOnce({
2764
+ ok: true,
2765
+ json: async () => ({
2766
+ sessions: [
2767
+ buildSessionResponse({
2768
+ globalInstruction: 'Review this screenshot',
2769
+ annotations: [
2770
+ {
2771
+ id: 'annotation-1',
2772
+ order: 0,
2773
+ kind: 'pin',
2774
+ x: 0.2,
2775
+ y: 0.3,
2776
+ instruction: 'Inspect this button',
2777
+ markerType: 'review',
2778
+ color: '#0f766e',
2779
+ },
2780
+ ],
2781
+ }),
2782
+ ],
2783
+ }),
2784
+ })
2785
+ .mockResolvedValueOnce({
2786
+ ok: false,
2787
+ json: async () => ({
2788
+ error: 'Save failed on the server.',
2789
+ }),
2790
+ })
2791
+ .mockResolvedValueOnce({
2792
+ ok: true,
2793
+ json: async () => ({
2794
+ session: buildSessionResponse({
2795
+ globalInstruction: 'Review this screenshot',
2796
+ updatedAt: '2026-03-25T12:07:00.000Z',
2797
+ annotations: [
2798
+ {
2799
+ id: 'annotation-1',
2800
+ order: 0,
2801
+ kind: 'pin',
2802
+ x: 0.2,
2803
+ y: 0.3,
2804
+ instruction: 'Inspect the purchase button',
2805
+ markerType: 'review',
2806
+ color: '#0f766e',
2807
+ },
2808
+ ],
2809
+ }),
2810
+ }),
2811
+ });
2812
+ render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2813
+ taskId: 'task-1',
2814
+ taskReferenceLabel: 'T-101',
2815
+ assetId: 'asset-image-1',
2816
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2817
+ displayName: 'Homepage shot',
2818
+ } }));
2819
+ expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2820
+ const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2821
+ fireEvent.change(instructionField, { target: { value: 'Inspect the purchase button' } });
2822
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2823
+ expect(screen.getByDisplayValue('Inspect the purchase button')).toBeInTheDocument();
2824
+ expect(screen.getByText('Save failed. Retry now.')).toBeInTheDocument();
2825
+ await user.click(screen.getByRole('button', { name: /save session/i }));
2826
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2827
+ const retryBody = JSON.parse(String(fetchTaskforceApi.mock.calls[2]?.[1]?.body || '{}'));
2828
+ expect(retryBody.annotations[0]?.instruction).toBe('Inspect the purchase button');
2829
+ await waitFor(() => expect(screen.queryByText('Save failed. Retry now.')).not.toBeInTheDocument());
2830
+ });
2831
+ });