@taskforcehq/taskforce 0.3.170 → 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 +2 -2
  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,1942 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { ArrowDown, ArrowLeft, ArrowUpLeft, ArrowUp, Check, ClipboardPaste, Copy, FolderOpen, Hand, HelpCircle, Loader2, MessageSquareCode, MessageSquareText, MousePointer2, Pin, Plus, RotateCcw, Save, SquareMousePointer, Trash2, ZoomIn, ZoomOut, } from 'lucide-react';
4
+ import sharedStyles from '../../Taskforce.module.css';
5
+ import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
6
+ import { logBootstrapDebug } from '../../hooks/workspace/workspaceLocalState';
7
+ import { Modal } from '../ui/Modal';
8
+ import { ImageReferenceBadge } from '../ui/ImageReferenceBadge';
9
+ import { TaskReferenceBadge } from '../task/TaskReferenceBadge';
10
+ import styles from './AnnotatedAttachmentWorkspace.module.css';
11
+ const MARKER_TYPE_OPTIONS = [
12
+ { value: 'review', label: 'Review' },
13
+ { value: 'change', label: 'Change' },
14
+ { value: 'question', label: 'Question' },
15
+ ];
16
+ const TOOL_OPTIONS = [
17
+ { value: 'select', label: 'Select', icon: MousePointer2 },
18
+ { value: 'pin', label: 'Pin', icon: Pin },
19
+ { value: 'box', label: 'Box', icon: SquareMousePointer },
20
+ { value: 'arrow', label: 'Arrow', icon: ArrowUpLeft },
21
+ { value: 'text-note', label: 'Note', icon: MessageSquareText },
22
+ ];
23
+ const ANNOTATION_KIND_ICON = {
24
+ pin: Pin,
25
+ box: SquareMousePointer,
26
+ arrow: ArrowUpLeft,
27
+ 'text-note': MessageSquareText,
28
+ };
29
+ const ANNOTATION_KIND_LABELS = {
30
+ pin: 'Pin',
31
+ box: 'Box',
32
+ arrow: 'Arrow',
33
+ 'text-note': 'Note',
34
+ };
35
+ const MARKER_TYPE_ICON = {
36
+ review: MessageSquareCode,
37
+ change: Check,
38
+ question: HelpCircle,
39
+ issue: HelpCircle,
40
+ idea: MessageSquareCode,
41
+ };
42
+ const TOOL_HELP_COPY = {
43
+ select: {
44
+ short: 'Select and edit existing markers.',
45
+ detail: 'Use Select to click, drag, reorder, resize, and update markers that are already on the image.',
46
+ example: 'Example: move an existing marker after the screenshot changes.',
47
+ },
48
+ pin: {
49
+ short: 'Mark a precise spot.',
50
+ detail: 'Use Pin when feedback points to one exact location instead of a broader area.',
51
+ example: 'Example: "This icon is misaligned by 2px."',
52
+ },
53
+ box: {
54
+ short: 'Mark an area or component.',
55
+ detail: 'Use Box when the feedback applies to a whole region, card, panel, or bounded UI block.',
56
+ example: 'Example: "This whole card needs tighter padding and a stronger border."',
57
+ },
58
+ arrow: {
59
+ short: 'Show direction or relationship.',
60
+ detail: 'Use Arrow when you need to show movement, attachment, flow, or source-to-target intent.',
61
+ example: 'Example: "This tooltip should anchor to this button, not the panel."',
62
+ },
63
+ 'text-note': {
64
+ short: 'Add a comment-style point marker.',
65
+ detail: 'Use Note when you want a point marker that reads more like a comment or open question.',
66
+ example: 'Example: "Ask design whether this badge should stay."',
67
+ },
68
+ };
69
+ const ANNOTATION_COLORS = {
70
+ question: '#0f766e',
71
+ change: '#2563eb',
72
+ issue: '#dc2626',
73
+ idea: '#d97706',
74
+ review: '#7c3aed',
75
+ };
76
+ const MARKER_COLOR_SWATCHES = [
77
+ '#7c3aed',
78
+ '#2563eb',
79
+ '#0f766e',
80
+ '#dc2626',
81
+ '#d97706',
82
+ '#111827',
83
+ ];
84
+ const DEFAULT_MARKER_TYPE = 'review';
85
+ function buildSessionTitle(target) {
86
+ const displayName = String(target.displayName || '').trim();
87
+ return displayName ? `${displayName} review` : 'Annotated session';
88
+ }
89
+ function buildAnnotationId() {
90
+ return `annotation-${Math.random().toString(36).slice(2, 10)}`;
91
+ }
92
+ function clamp01(value) {
93
+ if (!Number.isFinite(value))
94
+ return 0;
95
+ if (value <= 0)
96
+ return 0;
97
+ if (value >= 1)
98
+ return 1;
99
+ return value;
100
+ }
101
+ function clampSize(value) {
102
+ return clamp01(Math.max(0.02, value));
103
+ }
104
+ function getAnnotatedTargetKey(target) {
105
+ if (!target)
106
+ return '';
107
+ return [
108
+ String(target.taskId || '').trim(),
109
+ String(target.assetId || '').trim(),
110
+ String(target.path || '').trim(),
111
+ ].join('::');
112
+ }
113
+ function isEditableKeyboardTarget(target) {
114
+ if (!(target instanceof HTMLElement))
115
+ return false;
116
+ const tagName = target.tagName.toLowerCase();
117
+ if (target.isContentEditable)
118
+ return true;
119
+ return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
120
+ }
121
+ function normalizeOrder(annotations) {
122
+ return annotations.map((annotation, index) => ({
123
+ ...annotation,
124
+ order: index,
125
+ }));
126
+ }
127
+ function formatTimestamp(value) {
128
+ if (!value)
129
+ return 'Unsaved';
130
+ const parsed = new Date(value);
131
+ if (Number.isNaN(parsed.getTime()))
132
+ return 'Unsaved';
133
+ return parsed.toLocaleString();
134
+ }
135
+ function getSessionCreatorLabel(session) {
136
+ const createdByLabel = String(session.createdByActor?.label || '').trim();
137
+ if (createdByLabel)
138
+ return createdByLabel;
139
+ return null;
140
+ }
141
+ function sessionInstructionPreview(value) {
142
+ const normalized = String(value || '').trim().replace(/\s+/g, ' ');
143
+ if (!normalized)
144
+ return '';
145
+ return normalized.length > 110 ? `${normalized.slice(0, 107)}...` : normalized;
146
+ }
147
+ function annotationSummary(annotation) {
148
+ if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
149
+ return `${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%`;
150
+ }
151
+ if (annotation.kind === 'box') {
152
+ return `${Math.round(annotation.width * 100)}% × ${Math.round(annotation.height * 100)}%`;
153
+ }
154
+ return `${Math.round(Math.abs(annotation.x2 - annotation.x) * 100)}% span`;
155
+ }
156
+ function createAnnotation(kind, start, end, color = ANNOTATION_COLORS[DEFAULT_MARKER_TYPE]) {
157
+ const base = {
158
+ id: buildAnnotationId(),
159
+ order: 0,
160
+ instruction: '',
161
+ markerType: DEFAULT_MARKER_TYPE,
162
+ color,
163
+ };
164
+ if (kind === 'pin') {
165
+ return { ...base, kind, x: start.x, y: start.y };
166
+ }
167
+ if (kind === 'text-note') {
168
+ return { ...base, kind, x: start.x, y: start.y };
169
+ }
170
+ if (kind === 'box') {
171
+ const endPoint = end || start;
172
+ return {
173
+ ...base,
174
+ kind,
175
+ x: clamp01(Math.min(start.x, endPoint.x)),
176
+ y: clamp01(Math.min(start.y, endPoint.y)),
177
+ width: clampSize(Math.abs(endPoint.x - start.x)),
178
+ height: clampSize(Math.abs(endPoint.y - start.y)),
179
+ };
180
+ }
181
+ const endPoint = end || start;
182
+ return {
183
+ ...base,
184
+ kind: 'arrow',
185
+ x: start.x,
186
+ y: start.y,
187
+ x2: endPoint.x,
188
+ y2: endPoint.y,
189
+ };
190
+ }
191
+ function mapMarkerColor(annotation) {
192
+ if (annotation.color)
193
+ return annotation.color;
194
+ return ANNOTATION_COLORS[annotation.markerType || DEFAULT_MARKER_TYPE];
195
+ }
196
+ function getArrowGeometry(annotation, dimensions) {
197
+ const width = Math.max(dimensions.width, 1);
198
+ const height = Math.max(dimensions.height, 1);
199
+ const tailX = annotation.x * width;
200
+ const tailY = annotation.y * height;
201
+ const tipX = annotation.x2 * width;
202
+ const tipY = annotation.y2 * height;
203
+ const dx = tipX - tailX;
204
+ const dy = tipY - tailY;
205
+ const length = Math.hypot(dx, dy) || 1;
206
+ const unitX = dx / length;
207
+ const unitY = dy / length;
208
+ const headLength = Math.max(10, Math.min(16, length - 2));
209
+ const halfHeadWidth = headLength * 0.62;
210
+ const baseX = tipX - unitX * headLength;
211
+ const baseY = tipY - unitY * headLength;
212
+ const perpX = -unitY;
213
+ const perpY = unitX;
214
+ return {
215
+ shaftX1: tailX,
216
+ shaftY1: tailY,
217
+ shaftX2: baseX,
218
+ shaftY2: baseY,
219
+ headPoints: [
220
+ `${tipX},${tipY}`,
221
+ `${baseX + perpX * halfHeadWidth},${baseY + perpY * halfHeadWidth}`,
222
+ `${baseX - perpX * halfHeadWidth},${baseY - perpY * halfHeadWidth}`,
223
+ ].join(' '),
224
+ };
225
+ }
226
+ function updateMarkerType(annotation, markerType) {
227
+ return {
228
+ ...annotation,
229
+ markerType,
230
+ color: annotation.color || ANNOTATION_COLORS[markerType],
231
+ };
232
+ }
233
+ function cloneAnnotation(annotation, order) {
234
+ return {
235
+ ...annotation,
236
+ id: buildAnnotationId(),
237
+ order,
238
+ };
239
+ }
240
+ function buildDuplicateSessionTitle(title, fallbackTarget) {
241
+ const baseTitle = String(title || '').trim() || (fallbackTarget ? buildSessionTitle(fallbackTarget) : 'Annotated session');
242
+ if (/\bcopy$/i.test(baseTitle)) {
243
+ return `${baseTitle} 2`;
244
+ }
245
+ return `${baseTitle} copy`;
246
+ }
247
+ function moveAnnotation(annotations, fromIndex, toIndex) {
248
+ if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= annotations.length || toIndex >= annotations.length) {
249
+ return annotations;
250
+ }
251
+ const next = [...annotations];
252
+ const [moved] = next.splice(fromIndex, 1);
253
+ if (!moved)
254
+ return annotations;
255
+ next.splice(toIndex, 0, moved);
256
+ return normalizeOrder(next);
257
+ }
258
+ function toPercentField(value) {
259
+ return String(Math.round(clamp01(value) * 1000) / 10);
260
+ }
261
+ function parsePercentField(value) {
262
+ const numeric = Number.parseFloat(value);
263
+ if (!Number.isFinite(numeric))
264
+ return null;
265
+ return clamp01(numeric / 100);
266
+ }
267
+ function updateAnnotationGeometryField(annotation, field, value) {
268
+ if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
269
+ if (field === 'x' || field === 'y') {
270
+ return { ...annotation, [field]: clamp01(value) };
271
+ }
272
+ return annotation;
273
+ }
274
+ if (annotation.kind === 'box') {
275
+ if (field === 'x' || field === 'y') {
276
+ return { ...annotation, [field]: clamp01(value) };
277
+ }
278
+ if (field === 'width' || field === 'height') {
279
+ return { ...annotation, [field]: clampSize(value) };
280
+ }
281
+ return annotation;
282
+ }
283
+ if (field === 'x' || field === 'y' || field === 'x2' || field === 'y2') {
284
+ return { ...annotation, [field]: clamp01(value) };
285
+ }
286
+ return annotation;
287
+ }
288
+ function getGeometryFields(annotation) {
289
+ if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
290
+ return [
291
+ { key: 'x', label: 'X', value: toPercentField(annotation.x) },
292
+ { key: 'y', label: 'Y', value: toPercentField(annotation.y) },
293
+ ];
294
+ }
295
+ if (annotation.kind === 'box') {
296
+ return [
297
+ { key: 'x', label: 'X', value: toPercentField(annotation.x) },
298
+ { key: 'y', label: 'Y', value: toPercentField(annotation.y) },
299
+ { key: 'width', label: 'Width', value: toPercentField(annotation.width) },
300
+ { key: 'height', label: 'Height', value: toPercentField(annotation.height) },
301
+ ];
302
+ }
303
+ return [
304
+ { key: 'x', label: 'Start X', value: toPercentField(annotation.x) },
305
+ { key: 'y', label: 'Start Y', value: toPercentField(annotation.y) },
306
+ { key: 'x2', label: 'End X', value: toPercentField(annotation.x2) },
307
+ { key: 'y2', label: 'End Y', value: toPercentField(annotation.y2) },
308
+ ];
309
+ }
310
+ function formatCropHint(cropHint) {
311
+ if (!cropHint)
312
+ return null;
313
+ const left = Math.round(cropHint.x * 100);
314
+ const top = Math.round(cropHint.y * 100);
315
+ const width = Math.round(cropHint.width * 100);
316
+ const height = Math.round(cropHint.height * 100);
317
+ return `crop ${left}%, ${top}% size ${width}% x ${height}%`;
318
+ }
319
+ function clampZoom(value) {
320
+ if (!Number.isFinite(value))
321
+ return 1;
322
+ return Math.min(4, Math.max(0.25, Number(value.toFixed(2))));
323
+ }
324
+ function buildPayloadBrief(payload) {
325
+ const lines = [
326
+ `Annotated attachment: ${payload.title || payload.image.displayName}`,
327
+ `Image: ${payload.image.displayName}`,
328
+ `Image Reference: ${payload.image.referenceLabel || payload.image.assetId}`,
329
+ `Task ID: ${payload.taskId}`,
330
+ payload.globalInstruction
331
+ ? `Global instruction: ${payload.globalInstruction}`
332
+ : 'Global instruction: None provided.',
333
+ 'Markers:'
334
+ ];
335
+ if (payload.annotations.length === 0) {
336
+ lines.push('0. No markers.');
337
+ return lines.join('\n');
338
+ }
339
+ payload.annotations.forEach((annotation, index) => {
340
+ const markerType = annotation.markerType || DEFAULT_MARKER_TYPE;
341
+ const cropText = formatCropHint(annotation.cropHint);
342
+ lines.push(`${index + 1}. ${annotation.kind} (${markerType})`);
343
+ lines.push(`Instruction: ${annotation.instruction || 'No marker instruction.'}`);
344
+ if (cropText)
345
+ lines.push(`Region: ${cropText}`);
346
+ });
347
+ return lines.join('\n');
348
+ }
349
+ function normalizeDraftPayload(payload) {
350
+ return {
351
+ title: payload.title,
352
+ globalInstruction: payload.globalInstruction,
353
+ annotations: normalizeOrder(payload.annotations),
354
+ };
355
+ }
356
+ function serializeDraftPayload(payload) {
357
+ return JSON.stringify(normalizeDraftPayload(payload));
358
+ }
359
+ function parseDraftSnapshot(snapshot) {
360
+ const parsed = JSON.parse(snapshot);
361
+ return normalizeDraftPayload({
362
+ title: String(parsed?.title || ''),
363
+ globalInstruction: String(parsed?.globalInstruction || ''),
364
+ annotations: Array.isArray(parsed?.annotations) ? parsed.annotations : [],
365
+ });
366
+ }
367
+ function buildDraftPayloadFromSession(session, fallbackTitle) {
368
+ return normalizeDraftPayload({
369
+ title: session?.title || fallbackTitle,
370
+ globalInstruction: session?.globalInstruction || '',
371
+ annotations: session?.annotations || [],
372
+ });
373
+ }
374
+ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', workspaceId = 'default', sessionLoadReady = true, requestedTarget = null, requestedSessionId = null, requestedOpenVersion = 0, resolveTaskReferenceLabel, resolveImageReferenceLabel, onRequestedTargetHandled, onOpenTarget, onContextChange, onBackToTask, }) {
375
+ const resolvedApiBase = runtimeMode === 'cloud'
376
+ ? (cloudAuthBaseUrl || apiBaseUrl || '')
377
+ : '';
378
+ const [uncontrolledTarget, setUncontrolledTarget] = useState(requestedTarget);
379
+ const [sessions, setSessions] = useState([]);
380
+ const [selectedSessionId, setSelectedSessionId] = useState(null);
381
+ const [sessionCardEditing, setSessionCardEditing] = useState(false);
382
+ const [draftTitle, setDraftTitle] = useState('');
383
+ const [draftGlobalInstruction, setDraftGlobalInstruction] = useState('');
384
+ const [draftAnnotations, setDraftAnnotations] = useState([]);
385
+ const [selectedAnnotationId, setSelectedAnnotationId] = useState(null);
386
+ const [defaultMarkerColor, setDefaultMarkerColor] = useState(ANNOTATION_COLORS[DEFAULT_MARKER_TYPE]);
387
+ const [colorPickerOpen, setColorPickerOpen] = useState(false);
388
+ const [toolMode, setToolMode] = useState('select');
389
+ const [showCreateSessionPrompt, setShowCreateSessionPrompt] = useState(false);
390
+ const [loading, setLoading] = useState(false);
391
+ const [saving, setSaving] = useState(false);
392
+ const [error, setError] = useState(null);
393
+ const [saveState, setSaveState] = useState('saved');
394
+ const [lastSaveError, setLastSaveError] = useState(null);
395
+ const [payloadLoading, setPayloadLoading] = useState(false);
396
+ const [payloadPreview, setPayloadPreview] = useState(null);
397
+ const [payloadCopied, setPayloadCopied] = useState(false);
398
+ const [payloadModalOpen, setPayloadModalOpen] = useState(false);
399
+ const [payloadPreviewMode, setPayloadPreviewMode] = useState('json');
400
+ const [taskIdCopied, setTaskIdCopied] = useState(false);
401
+ const [imageIdCopied, setImageIdCopied] = useState(false);
402
+ const [toolHelpModalOpen, setToolHelpModalOpen] = useState(false);
403
+ const [openImageModalOpen, setOpenImageModalOpen] = useState(false);
404
+ const [openImageReference, setOpenImageReference] = useState('');
405
+ const [openingImage, setOpeningImage] = useState(false);
406
+ const [imageLoading, setImageLoading] = useState(false);
407
+ const [imageLoadError, setImageLoadError] = useState(false);
408
+ const [zoomLevel, setZoomLevel] = useState(1);
409
+ const [panMode, setPanMode] = useState(false);
410
+ const [pastingImage, setPastingImage] = useState(false);
411
+ const [naturalImageSize, setNaturalImageSize] = useState({ width: 0, height: 0 });
412
+ const overlayRef = useRef(null);
413
+ const canvasScrollerRef = useRef(null);
414
+ const imageRef = useRef(null);
415
+ const selectedInstructionTextareaRef = useRef(null);
416
+ const pendingRequestedSessionIdRef = useRef(requestedSessionId);
417
+ const lastLoadedTargetKeyRef = useRef('');
418
+ const lastHandledRequestedOpenVersionRef = useRef(0);
419
+ const deferredTargetKeyRef = useRef('');
420
+ const latestLoadRequestIdRef = useRef(0);
421
+ const activeLoadKeyRef = useRef(null);
422
+ const suppressVisibilityReloadUntilRef = useRef(0);
423
+ const suppressAutoSaveRef = useRef(false);
424
+ const debounceTimerRef = useRef(null);
425
+ const pendingDebounceMsRef = useRef(null);
426
+ const selectedSessionIdRef = useRef(null);
427
+ const currentDraftSnapshotRef = useRef('');
428
+ const lastSavedSnapshotRef = useRef('');
429
+ const inFlightSnapshotRef = useRef(null);
430
+ const inFlightPromiseRef = useRef(null);
431
+ const hasPendingSaveAfterFlightRef = useRef(false);
432
+ const activeSaveSessionIdRef = useRef(null);
433
+ const autoRetrySnapshotRef = useRef(null);
434
+ const retryTimerRef = useRef(null);
435
+ const gestureStartRef = useRef(null);
436
+ const dragRef = useRef(null);
437
+ const resizeRef = useRef(null);
438
+ const arrowHandleRef = useRef(null);
439
+ const creatingAnnotationRef = useRef(null);
440
+ const panRef = useRef(null);
441
+ const colorPickerRef = useRef(null);
442
+ const sessionCardRef = useRef(null);
443
+ const selectedSession = useMemo(() => sessions.find((session) => session.id === selectedSessionId) || null, [selectedSessionId, sessions]);
444
+ const workspaceQuery = useMemo(() => `workspaceId=${encodeURIComponent(String(workspaceId || 'default').trim() || 'default')}`, [workspaceId]);
445
+ const requestHeaders = useMemo(() => ({ 'x-taskforce-workspace-id': String(workspaceId || 'default').trim() || 'default' }), [workspaceId]);
446
+ const selectedAnnotation = useMemo(() => draftAnnotations.find((annotation) => annotation.id === selectedAnnotationId) || null, [draftAnnotations, selectedAnnotationId]);
447
+ const requestedTargetKey = useMemo(() => getAnnotatedTargetKey(requestedTarget), [requestedTarget]);
448
+ const isTargetControlled = typeof onOpenTarget === 'function';
449
+ const activeTarget = isTargetControlled ? requestedTarget : uncontrolledTarget;
450
+ const activeTaskReferenceLabel = useMemo(() => {
451
+ const taskId = String(activeTarget?.taskId || '').trim();
452
+ if (!taskId)
453
+ return '';
454
+ const resolvedLabel = resolveTaskReferenceLabel?.(taskId).trim() || '';
455
+ if (resolvedLabel)
456
+ return resolvedLabel;
457
+ const explicitLabel = String(activeTarget?.taskReferenceLabel || '').trim();
458
+ if (explicitLabel && explicitLabel !== taskId)
459
+ return explicitLabel;
460
+ return resolvedLabel || taskId;
461
+ }, [activeTarget?.taskId, activeTarget?.taskReferenceLabel, resolveTaskReferenceLabel]);
462
+ const activeImageReferenceLabel = useMemo(() => {
463
+ const assetId = String(activeTarget?.assetId || '').trim();
464
+ if (!assetId)
465
+ return '';
466
+ const resolvedLabel = resolveImageReferenceLabel?.(assetId).trim() || '';
467
+ if (resolvedLabel)
468
+ return resolvedLabel;
469
+ return String(activeTarget?.imageReferenceLabel || '').trim();
470
+ }, [activeTarget?.assetId, activeTarget?.imageReferenceLabel, resolveImageReferenceLabel]);
471
+ const selectedAnnotationIndex = useMemo(() => draftAnnotations.findIndex((annotation) => annotation.id === selectedAnnotationId), [draftAnnotations, selectedAnnotationId]);
472
+ const selectedMarkerColor = selectedAnnotation?.color || defaultMarkerColor;
473
+ const currentDraftPayload = useMemo(() => normalizeDraftPayload({
474
+ title: draftTitle,
475
+ globalInstruction: draftGlobalInstruction,
476
+ annotations: draftAnnotations,
477
+ }), [draftAnnotations, draftGlobalInstruction, draftTitle]);
478
+ const currentDraftSnapshot = useMemo(() => serializeDraftPayload(currentDraftPayload), [currentDraftPayload]);
479
+ const hasUnsavedChanges = currentDraftSnapshot !== lastSavedSnapshotRef.current;
480
+ const canvasMediaSize = useMemo(() => ({
481
+ width: Math.max(naturalImageSize.width * zoomLevel, 0),
482
+ height: Math.max(naturalImageSize.height * zoomLevel, 0),
483
+ }), [naturalImageSize.height, naturalImageSize.width, zoomLevel]);
484
+ const arrowHandleGeometry = useMemo(() => ({
485
+ visibleRadius: 7,
486
+ hitRadius: 11,
487
+ }), []);
488
+ const arrowOverlayViewBox = useMemo(() => `0 0 ${Math.max(canvasMediaSize.width, 1)} ${Math.max(canvasMediaSize.height, 1)}`, [canvasMediaSize.height, canvasMediaSize.width]);
489
+ const canPan = useMemo(() => {
490
+ const scroller = canvasScrollerRef.current;
491
+ if (!scroller)
492
+ return zoomLevel > 1;
493
+ return (zoomLevel > 1
494
+ || (canvasMediaSize.width > (scroller.clientWidth + 1)
495
+ || canvasMediaSize.height > (scroller.clientHeight + 1)));
496
+ }, [canvasMediaSize.height, canvasMediaSize.width, zoomLevel]);
497
+ const zoomLabel = `${Math.round(zoomLevel * 100)}%`;
498
+ const annotationOrderNumbers = useMemo(() => {
499
+ const orderMap = new Map();
500
+ draftAnnotations.forEach((annotation, index) => {
501
+ orderMap.set(annotation.id, index + 1);
502
+ });
503
+ return orderMap;
504
+ }, [draftAnnotations]);
505
+ useEffect(() => {
506
+ pendingRequestedSessionIdRef.current = requestedSessionId;
507
+ }, [requestedSessionId]);
508
+ useEffect(() => {
509
+ const textarea = selectedInstructionTextareaRef.current;
510
+ if (!textarea)
511
+ return;
512
+ if (!selectedAnnotationId)
513
+ return;
514
+ textarea.focus();
515
+ const end = textarea.value.length;
516
+ textarea.setSelectionRange(end, end);
517
+ }, [selectedAnnotationId]);
518
+ useEffect(() => {
519
+ selectedSessionIdRef.current = selectedSessionId;
520
+ }, [selectedSessionId]);
521
+ useEffect(() => {
522
+ setSessionCardEditing(false);
523
+ }, [selectedSessionId]);
524
+ useEffect(() => {
525
+ if (!selectedAnnotation) {
526
+ setColorPickerOpen(false);
527
+ return;
528
+ }
529
+ setDefaultMarkerColor(selectedAnnotation.color || ANNOTATION_COLORS[selectedAnnotation.markerType || DEFAULT_MARKER_TYPE]);
530
+ }, [selectedAnnotation]);
531
+ const createSessionForTarget = useCallback(async (target) => {
532
+ const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
533
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${workspaceQuery}`, {
534
+ method: 'POST',
535
+ credentials: 'include',
536
+ headers: { 'Content-Type': 'application/json', ...requestHeaders },
537
+ body: JSON.stringify({
538
+ taskId: target.taskId,
539
+ baseImageAssetId: target.assetId,
540
+ title: buildSessionTitle(target),
541
+ globalInstruction: '',
542
+ annotations: [],
543
+ }),
544
+ }, resolvedApiBase);
545
+ const data = await res.json().catch(() => ({}));
546
+ if (!res.ok) {
547
+ throw new Error(String(data?.error || 'Failed to create session.'));
548
+ }
549
+ const session = data?.session;
550
+ if (!session?.id) {
551
+ throw new Error('Failed to create session.');
552
+ }
553
+ logBootstrapDebug('annotated_session_create_completed', {
554
+ assetId: target.assetId,
555
+ taskId: target.taskId || null,
556
+ sessionId: session.id,
557
+ durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
558
+ debugTimings: data?.debugTimings || null,
559
+ serverTiming: typeof res.headers?.get === 'function'
560
+ ? res.headers.get('server-timing') || null
561
+ : null,
562
+ });
563
+ return session;
564
+ }, [requestHeaders, resolvedApiBase, workspaceQuery]);
565
+ const readFileAsDataUrl = useCallback((blob) => new Promise((resolve, reject) => {
566
+ const reader = new FileReader();
567
+ reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
568
+ reader.onerror = () => reject(reader.error || new Error('Failed to read clipboard image.'));
569
+ reader.readAsDataURL(blob);
570
+ }), []);
571
+ const clearPendingAutoSave = useCallback(() => {
572
+ if (debounceTimerRef.current !== null) {
573
+ window.clearTimeout(debounceTimerRef.current);
574
+ debounceTimerRef.current = null;
575
+ }
576
+ if (retryTimerRef.current !== null) {
577
+ window.clearTimeout(retryTimerRef.current);
578
+ retryTimerRef.current = null;
579
+ }
580
+ }, []);
581
+ const applyDraftPayload = useCallback((payload) => {
582
+ setDraftTitle(payload.title);
583
+ setDraftGlobalInstruction(payload.globalInstruction);
584
+ setDraftAnnotations(payload.annotations);
585
+ setSelectedAnnotationId((current) => (current && payload.annotations.some((annotation) => annotation.id === current)
586
+ ? current
587
+ : (payload.annotations[0]?.id || null)));
588
+ }, []);
589
+ const syncDraftFromSession = useCallback((session, target) => {
590
+ const fallbackTitle = buildSessionTitle(target || activeTarget || {
591
+ taskId: '',
592
+ assetId: session.baseImageAssetId,
593
+ path: '',
594
+ displayName: 'Annotated session',
595
+ });
596
+ const payload = buildDraftPayloadFromSession(session, fallbackTitle);
597
+ const snapshot = serializeDraftPayload(payload);
598
+ suppressAutoSaveRef.current = true;
599
+ clearPendingAutoSave();
600
+ applyDraftPayload(payload);
601
+ lastSavedSnapshotRef.current = snapshot;
602
+ currentDraftSnapshotRef.current = snapshot;
603
+ inFlightSnapshotRef.current = null;
604
+ hasPendingSaveAfterFlightRef.current = false;
605
+ activeSaveSessionIdRef.current = null;
606
+ autoRetrySnapshotRef.current = null;
607
+ setLastSaveError(null);
608
+ setSaveState('saved');
609
+ setSaving(false);
610
+ }, [activeTarget, applyDraftPayload, clearPendingAutoSave]);
611
+ const resetForTargetLoad = useCallback((target) => {
612
+ suppressAutoSaveRef.current = true;
613
+ clearPendingAutoSave();
614
+ setSessions([]);
615
+ setSelectedSessionId(null);
616
+ setShowCreateSessionPrompt(false);
617
+ setSessionCardEditing(false);
618
+ setDraftTitle(buildSessionTitle(target));
619
+ setDraftGlobalInstruction('');
620
+ setDraftAnnotations([]);
621
+ setSelectedAnnotationId(null);
622
+ lastSavedSnapshotRef.current = '';
623
+ currentDraftSnapshotRef.current = '';
624
+ inFlightSnapshotRef.current = null;
625
+ hasPendingSaveAfterFlightRef.current = false;
626
+ activeSaveSessionIdRef.current = null;
627
+ autoRetrySnapshotRef.current = null;
628
+ setPayloadPreview(null);
629
+ setPayloadModalOpen(false);
630
+ setLastSaveError(null);
631
+ setSaveState('saved');
632
+ setSaving(false);
633
+ }, [clearPendingAutoSave]);
634
+ const saveSnapshot = useCallback(async (snapshot, trigger, options) => {
635
+ const sessionId = selectedSessionIdRef.current;
636
+ if (!sessionId)
637
+ return true;
638
+ if (snapshot === lastSavedSnapshotRef.current) {
639
+ if (!inFlightPromiseRef.current) {
640
+ setLastSaveError(null);
641
+ setSaveState('saved');
642
+ }
643
+ return true;
644
+ }
645
+ if (inFlightPromiseRef.current) {
646
+ hasPendingSaveAfterFlightRef.current = true;
647
+ if (!options?.waitForInFlight)
648
+ return false;
649
+ const inFlightResult = await inFlightPromiseRef.current;
650
+ if (!inFlightResult)
651
+ return false;
652
+ const latestSnapshot = currentDraftSnapshotRef.current;
653
+ if (latestSnapshot === lastSavedSnapshotRef.current)
654
+ return true;
655
+ return saveSnapshot(latestSnapshot, trigger, options);
656
+ }
657
+ clearPendingAutoSave();
658
+ const payload = parseDraftSnapshot(snapshot);
659
+ activeSaveSessionIdRef.current = sessionId;
660
+ inFlightSnapshotRef.current = snapshot;
661
+ setSaving(true);
662
+ setError(null);
663
+ setLastSaveError(null);
664
+ setSaveState('saving');
665
+ const request = (async () => {
666
+ try {
667
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(sessionId)}?${workspaceQuery}`, {
668
+ method: 'PATCH',
669
+ credentials: 'include',
670
+ headers: { 'Content-Type': 'application/json', ...requestHeaders },
671
+ body: JSON.stringify(payload),
672
+ }, resolvedApiBase);
673
+ const data = await res.json().catch(() => ({}));
674
+ if (!res.ok) {
675
+ throw new Error(String(data?.error || 'Failed to save session.'));
676
+ }
677
+ const session = data?.session;
678
+ if (!session?.id)
679
+ throw new Error('Failed to save session.');
680
+ setSessions((current) => {
681
+ const existingIndex = current.findIndex((item) => item.id === session.id);
682
+ if (existingIndex === -1)
683
+ return [session, ...current];
684
+ const next = [...current];
685
+ next[existingIndex] = session;
686
+ return next;
687
+ });
688
+ lastSavedSnapshotRef.current = snapshot;
689
+ autoRetrySnapshotRef.current = null;
690
+ setLastSaveError(null);
691
+ const stillCurrentSession = selectedSessionIdRef.current === session.id;
692
+ const latestSnapshot = currentDraftSnapshotRef.current;
693
+ const hasNewerLocalChanges = latestSnapshot !== snapshot;
694
+ const shouldSaveAgain = hasPendingSaveAfterFlightRef.current || hasNewerLocalChanges;
695
+ hasPendingSaveAfterFlightRef.current = false;
696
+ if (stillCurrentSession && !shouldSaveAgain) {
697
+ suppressAutoSaveRef.current = true;
698
+ applyDraftPayload(payload);
699
+ setSaveState('saved');
700
+ }
701
+ else if (!shouldSaveAgain && latestSnapshot === lastSavedSnapshotRef.current) {
702
+ setSaveState('saved');
703
+ }
704
+ else {
705
+ setSaveState('pending');
706
+ }
707
+ return true;
708
+ }
709
+ catch (requestError) {
710
+ const message = requestError instanceof Error ? requestError.message : 'Failed to save session.';
711
+ setError(message);
712
+ setLastSaveError(message);
713
+ setSaveState('error');
714
+ if (autoRetrySnapshotRef.current !== snapshot
715
+ && selectedSessionIdRef.current === sessionId
716
+ && currentDraftSnapshotRef.current === snapshot) {
717
+ autoRetrySnapshotRef.current = snapshot;
718
+ retryTimerRef.current = window.setTimeout(() => {
719
+ retryTimerRef.current = null;
720
+ if (selectedSessionIdRef.current !== sessionId
721
+ || currentDraftSnapshotRef.current !== snapshot) {
722
+ return;
723
+ }
724
+ void saveSnapshot(snapshot, trigger, { waitForInFlight: true });
725
+ }, 1500);
726
+ }
727
+ return false;
728
+ }
729
+ finally {
730
+ inFlightSnapshotRef.current = null;
731
+ inFlightPromiseRef.current = null;
732
+ activeSaveSessionIdRef.current = null;
733
+ setSaving(false);
734
+ }
735
+ })();
736
+ inFlightPromiseRef.current = request;
737
+ const success = await request;
738
+ if (success) {
739
+ const latestSnapshot = currentDraftSnapshotRef.current;
740
+ if (latestSnapshot !== lastSavedSnapshotRef.current) {
741
+ return saveSnapshot(latestSnapshot, trigger, options);
742
+ }
743
+ }
744
+ return success;
745
+ }, [applyDraftPayload, clearPendingAutoSave, requestHeaders, resolvedApiBase, workspaceQuery]);
746
+ const scheduleAutoSave = useCallback((delayMs) => {
747
+ if (!selectedSessionIdRef.current)
748
+ return;
749
+ if (currentDraftSnapshotRef.current === lastSavedSnapshotRef.current) {
750
+ setLastSaveError(null);
751
+ if (!inFlightPromiseRef.current)
752
+ setSaveState('saved');
753
+ return;
754
+ }
755
+ if (saveState !== 'saving')
756
+ setSaveState('pending');
757
+ clearPendingAutoSave();
758
+ debounceTimerRef.current = window.setTimeout(() => {
759
+ debounceTimerRef.current = null;
760
+ void saveSnapshot(currentDraftSnapshotRef.current, 'structure');
761
+ }, delayMs);
762
+ }, [clearPendingAutoSave, saveSnapshot, saveState]);
763
+ const markDraftChanged = useCallback((delayMs) => {
764
+ pendingDebounceMsRef.current = delayMs;
765
+ }, []);
766
+ const flushPendingDraft = useCallback(async (trigger) => {
767
+ clearPendingAutoSave();
768
+ const currentSnapshot = currentDraftSnapshotRef.current;
769
+ if (!selectedSessionIdRef.current || currentSnapshot === lastSavedSnapshotRef.current) {
770
+ setLastSaveError(null);
771
+ if (!inFlightPromiseRef.current)
772
+ setSaveState('saved');
773
+ return true;
774
+ }
775
+ return saveSnapshot(currentSnapshot, trigger, { waitForInFlight: true });
776
+ }, [clearPendingAutoSave, saveSnapshot]);
777
+ const restoreLastSavedDraft = useCallback(() => {
778
+ const snapshot = lastSavedSnapshotRef.current;
779
+ if (!snapshot)
780
+ return;
781
+ clearPendingAutoSave();
782
+ suppressAutoSaveRef.current = true;
783
+ applyDraftPayload(parseDraftSnapshot(snapshot));
784
+ setLastSaveError(null);
785
+ setSaveState('saved');
786
+ setError(null);
787
+ }, [applyDraftPayload, clearPendingAutoSave]);
788
+ const loadSessions = useCallback(async (target, options) => {
789
+ const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
790
+ const loadKey = JSON.stringify({
791
+ targetKey: getAnnotatedTargetKey(target),
792
+ requestedSessionId: options?.requestedSessionId ?? null,
793
+ autoCreateIfEmpty: options?.autoCreateIfEmpty === true,
794
+ });
795
+ if (activeLoadKeyRef.current === loadKey)
796
+ return;
797
+ const requestId = latestLoadRequestIdRef.current + 1;
798
+ latestLoadRequestIdRef.current = requestId;
799
+ activeLoadKeyRef.current = loadKey;
800
+ setLoading(true);
801
+ setError(null);
802
+ try {
803
+ const params = new URLSearchParams();
804
+ if (workspaceId)
805
+ params.set('workspaceId', workspaceId);
806
+ if (target.taskId)
807
+ params.set('taskId', target.taskId);
808
+ params.set('imageAssetId', target.assetId);
809
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${params.toString()}`, { credentials: 'include', headers: requestHeaders, cache: 'no-store' }, resolvedApiBase);
810
+ const data = await res.json().catch(() => ({}));
811
+ if (!res.ok) {
812
+ throw new Error(String(data?.error || 'Failed to load annotated attachment sessions.'));
813
+ }
814
+ const nextSessions = Array.isArray(data?.sessions) ? data.sessions : [];
815
+ logBootstrapDebug('annotated_sessions_loaded', {
816
+ assetId: target.assetId,
817
+ taskId: target.taskId || null,
818
+ requestedSessionId: options?.requestedSessionId ?? null,
819
+ autoCreateIfEmpty: options?.autoCreateIfEmpty === true,
820
+ sessionCount: nextSessions.length,
821
+ durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
822
+ serverTiming: typeof res.headers?.get === 'function'
823
+ ? res.headers.get('server-timing') || null
824
+ : null,
825
+ });
826
+ if (nextSessions.length === 0
827
+ && options?.autoCreateIfEmpty
828
+ && String(target.assetId || '').trim()) {
829
+ const requestedSessionIdValue = options?.requestedSessionId ?? pendingRequestedSessionIdRef.current;
830
+ pendingRequestedSessionIdRef.current = null;
831
+ if (requestedSessionIdValue) {
832
+ setError('The previously selected annotation session could not be restored.');
833
+ }
834
+ if (latestLoadRequestIdRef.current !== requestId)
835
+ return;
836
+ const session = await createSessionForTarget(target);
837
+ logBootstrapDebug('annotated_sessions_auto_created_after_empty_load', {
838
+ assetId: target.assetId,
839
+ taskId: target.taskId || null,
840
+ sessionId: session.id,
841
+ totalDurationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
842
+ });
843
+ if (latestLoadRequestIdRef.current !== requestId)
844
+ return;
845
+ setSessions([session]);
846
+ setSelectedSessionId(session.id);
847
+ setShowCreateSessionPrompt(false);
848
+ syncDraftFromSession(session, target);
849
+ return;
850
+ }
851
+ if (latestLoadRequestIdRef.current !== requestId)
852
+ return;
853
+ setSessions(nextSessions);
854
+ setShowCreateSessionPrompt(nextSessions.length === 0);
855
+ const requestedSessionIdValue = options?.requestedSessionId ?? pendingRequestedSessionIdRef.current;
856
+ pendingRequestedSessionIdRef.current = null;
857
+ const nextSelected = nextSessions.find((session) => session.id === requestedSessionIdValue) || nextSessions[0] || null;
858
+ if (requestedSessionIdValue && !nextSelected) {
859
+ setError('The previously selected annotation session could not be restored.');
860
+ }
861
+ setSelectedSessionId(nextSelected?.id || null);
862
+ if (nextSelected) {
863
+ syncDraftFromSession(nextSelected, target);
864
+ }
865
+ else {
866
+ suppressAutoSaveRef.current = true;
867
+ clearPendingAutoSave();
868
+ setDraftTitle(buildSessionTitle(target));
869
+ setDraftGlobalInstruction('');
870
+ setDraftAnnotations([]);
871
+ setSelectedAnnotationId(null);
872
+ lastSavedSnapshotRef.current = '';
873
+ currentDraftSnapshotRef.current = '';
874
+ setLastSaveError(null);
875
+ setSaveState('saved');
876
+ }
877
+ }
878
+ catch (requestError) {
879
+ if (latestLoadRequestIdRef.current !== requestId)
880
+ return;
881
+ setError(requestError instanceof Error ? requestError.message : 'Failed to load sessions.');
882
+ }
883
+ finally {
884
+ if (activeLoadKeyRef.current === loadKey) {
885
+ activeLoadKeyRef.current = null;
886
+ }
887
+ if (latestLoadRequestIdRef.current === requestId) {
888
+ setLoading(false);
889
+ }
890
+ }
891
+ }, [clearPendingAutoSave, createSessionForTarget, requestHeaders, resolvedApiBase, syncDraftFromSession, workspaceId]);
892
+ const pasteImageFromClipboard = useCallback(async () => {
893
+ if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.read !== 'function') {
894
+ setError('Clipboard image paste is not supported in this environment.');
895
+ return;
896
+ }
897
+ setPastingImage(true);
898
+ setError(null);
899
+ suppressVisibilityReloadUntilRef.current = Date.now() + 2000;
900
+ try {
901
+ const items = await navigator.clipboard.read();
902
+ const imageItem = items.find((item) => item.types.some((type) => type.startsWith('image/')));
903
+ const imageType = imageItem?.types.find((type) => type.startsWith('image/')) || '';
904
+ if (!imageItem || !imageType) {
905
+ throw new Error('No image found on the clipboard.');
906
+ }
907
+ const blob = await imageItem.getType(imageType);
908
+ const encoded = await readFileAsDataUrl(blob);
909
+ if (!encoded) {
910
+ throw new Error('Failed to read clipboard image.');
911
+ }
912
+ const extension = imageType === 'image/jpeg'
913
+ ? 'jpg'
914
+ : imageType === 'image/webp'
915
+ ? 'webp'
916
+ : imageType === 'image/gif'
917
+ ? 'gif'
918
+ : 'png';
919
+ const response = await fetch('/api/taskforce/context-upload', {
920
+ method: 'POST',
921
+ credentials: 'include',
922
+ headers: {
923
+ 'Content-Type': 'application/json',
924
+ ...requestHeaders,
925
+ },
926
+ body: JSON.stringify({
927
+ file: encoded,
928
+ originalName: `pasted-image.${extension}`,
929
+ workspaceId: String(workspaceId || 'default').trim() || 'default',
930
+ }),
931
+ });
932
+ const data = await response.json().catch(() => ({}));
933
+ if (!response.ok || !data?.success || typeof data?.assetId !== 'string' || typeof data?.path !== 'string') {
934
+ throw new Error(String(data?.error || 'Failed to paste image into Image Notes.'));
935
+ }
936
+ const target = {
937
+ assetId: data.assetId,
938
+ imageReferenceLabel: typeof data?.referenceLabel === 'string' ? data.referenceLabel : undefined,
939
+ path: data.path,
940
+ displayName: typeof data?.displayName === 'string' && data.displayName.trim().length > 0
941
+ ? data.displayName.trim()
942
+ : 'Pasted image',
943
+ };
944
+ pendingRequestedSessionIdRef.current = null;
945
+ if (isTargetControlled) {
946
+ lastLoadedTargetKeyRef.current = '';
947
+ onOpenTarget?.(target, { sessionId: null });
948
+ }
949
+ else {
950
+ setUncontrolledTarget(target);
951
+ lastLoadedTargetKeyRef.current = getAnnotatedTargetKey(target);
952
+ void loadSessions(target, { autoCreateIfEmpty: true });
953
+ }
954
+ }
955
+ catch (pasteError) {
956
+ setError(pasteError instanceof Error ? pasteError.message : 'Failed to paste image.');
957
+ }
958
+ finally {
959
+ setPastingImage(false);
960
+ }
961
+ }, [isTargetControlled, loadSessions, onOpenTarget, readFileAsDataUrl, requestHeaders, workspaceId]);
962
+ const openImageByReference = useCallback(async () => {
963
+ const normalizedReference = openImageReference.trim();
964
+ if (!normalizedReference) {
965
+ setError('Enter an image reference to open.');
966
+ return;
967
+ }
968
+ setOpeningImage(true);
969
+ setError(null);
970
+ try {
971
+ const workspaceQuery = new URLSearchParams({
972
+ workspaceId: String(workspaceId || 'default').trim() || 'default',
973
+ });
974
+ const response = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/images/${encodeURIComponent(normalizedReference)}?${workspaceQuery.toString()}`, {
975
+ method: 'GET',
976
+ credentials: 'include',
977
+ headers: requestHeaders,
978
+ });
979
+ const data = await response.json().catch(() => ({}));
980
+ if (!response.ok || !data?.target || typeof data.target.assetId !== 'string' || typeof data.target.path !== 'string') {
981
+ throw new Error(String(data?.error || 'Failed to open image reference.'));
982
+ }
983
+ const target = {
984
+ assetId: data.target.assetId,
985
+ path: data.target.path,
986
+ displayName: typeof data.target.displayName === 'string' && data.target.displayName.trim().length > 0
987
+ ? data.target.displayName.trim()
988
+ : 'Image attachment',
989
+ taskId: typeof data.target.taskId === 'string' && data.target.taskId.trim().length > 0
990
+ ? data.target.taskId.trim()
991
+ : undefined,
992
+ taskReferenceLabel: typeof data.target.taskReferenceLabel === 'string' && data.target.taskReferenceLabel.trim().length > 0
993
+ ? data.target.taskReferenceLabel.trim()
994
+ : undefined,
995
+ imageReferenceLabel: typeof data.target.imageReferenceLabel === 'string' && data.target.imageReferenceLabel.trim().length > 0
996
+ ? data.target.imageReferenceLabel.trim()
997
+ : undefined,
998
+ };
999
+ pendingRequestedSessionIdRef.current = null;
1000
+ setOpenImageModalOpen(false);
1001
+ setOpenImageReference('');
1002
+ if (isTargetControlled) {
1003
+ lastLoadedTargetKeyRef.current = '';
1004
+ onOpenTarget?.(target, { sessionId: null });
1005
+ }
1006
+ else {
1007
+ setUncontrolledTarget(target);
1008
+ lastLoadedTargetKeyRef.current = getAnnotatedTargetKey(target);
1009
+ void loadSessions(target, { autoCreateIfEmpty: true });
1010
+ }
1011
+ }
1012
+ catch (openError) {
1013
+ setError(openError instanceof Error ? openError.message : 'Failed to open image reference.');
1014
+ }
1015
+ finally {
1016
+ setOpeningImage(false);
1017
+ }
1018
+ }, [isTargetControlled, loadSessions, onOpenTarget, openImageReference, requestHeaders, workspaceId]);
1019
+ const applySessionLocally = useCallback((session) => {
1020
+ setSessions((current) => {
1021
+ const existingIndex = current.findIndex((item) => item.id === session.id);
1022
+ if (existingIndex === -1)
1023
+ return [session, ...current];
1024
+ const next = [...current];
1025
+ next[existingIndex] = session;
1026
+ return next;
1027
+ });
1028
+ setSelectedSessionId(session.id);
1029
+ syncDraftFromSession(session, activeTarget);
1030
+ }, [activeTarget, syncDraftFromSession]);
1031
+ const removeSessionLocally = useCallback((sessionId) => {
1032
+ setSessions((current) => {
1033
+ const remaining = current.filter((session) => session.id !== sessionId);
1034
+ const nextSelected = remaining[0] || null;
1035
+ setSelectedSessionId(nextSelected?.id || null);
1036
+ if (nextSelected) {
1037
+ syncDraftFromSession(nextSelected, activeTarget);
1038
+ }
1039
+ else {
1040
+ suppressAutoSaveRef.current = true;
1041
+ clearPendingAutoSave();
1042
+ setDraftTitle(buildSessionTitle(activeTarget || {
1043
+ taskId: '',
1044
+ assetId: '',
1045
+ path: '',
1046
+ displayName: 'Annotated session',
1047
+ }));
1048
+ setDraftGlobalInstruction('');
1049
+ setDraftAnnotations([]);
1050
+ setSelectedAnnotationId(null);
1051
+ lastSavedSnapshotRef.current = '';
1052
+ currentDraftSnapshotRef.current = '';
1053
+ setLastSaveError(null);
1054
+ setSaveState('saved');
1055
+ }
1056
+ return remaining;
1057
+ });
1058
+ }, [activeTarget, clearPendingAutoSave, syncDraftFromSession]);
1059
+ useEffect(() => {
1060
+ if (!requestedTarget)
1061
+ return;
1062
+ if (!isTargetControlled) {
1063
+ setUncontrolledTarget((current) => {
1064
+ if (current
1065
+ && getAnnotatedTargetKey(current) === requestedTargetKey
1066
+ && current.taskReferenceLabel === requestedTarget.taskReferenceLabel
1067
+ && current.imageReferenceLabel === requestedTarget.imageReferenceLabel
1068
+ && current.displayName === requestedTarget.displayName) {
1069
+ return current;
1070
+ }
1071
+ return requestedTarget;
1072
+ });
1073
+ }
1074
+ if (!sessionLoadReady) {
1075
+ const shouldDeferRequestedTarget = Boolean(requestedTargetKey
1076
+ && (requestedTargetKey !== lastLoadedTargetKeyRef.current
1077
+ || requestedOpenVersion !== lastHandledRequestedOpenVersionRef.current)
1078
+ && requestedTargetKey !== deferredTargetKeyRef.current);
1079
+ if (shouldDeferRequestedTarget) {
1080
+ resetForTargetLoad(requestedTarget);
1081
+ deferredTargetKeyRef.current = requestedTargetKey;
1082
+ lastHandledRequestedOpenVersionRef.current = requestedOpenVersion;
1083
+ logBootstrapDebug('annotated_sessions_load_deferred', {
1084
+ assetId: requestedTarget.assetId,
1085
+ taskId: requestedTarget.taskId || null,
1086
+ requestedTargetKey,
1087
+ });
1088
+ }
1089
+ onRequestedTargetHandled?.();
1090
+ return;
1091
+ }
1092
+ const shouldReloadRequestedTarget = Boolean(requestedTargetKey
1093
+ && (requestedTargetKey !== lastLoadedTargetKeyRef.current
1094
+ || requestedOpenVersion !== lastHandledRequestedOpenVersionRef.current));
1095
+ if (shouldReloadRequestedTarget) {
1096
+ resetForTargetLoad(requestedTarget);
1097
+ lastLoadedTargetKeyRef.current = requestedTargetKey;
1098
+ lastHandledRequestedOpenVersionRef.current = requestedOpenVersion;
1099
+ deferredTargetKeyRef.current = '';
1100
+ void loadSessions(requestedTarget, { autoCreateIfEmpty: true });
1101
+ }
1102
+ onRequestedTargetHandled?.();
1103
+ }, [isTargetControlled, loadSessions, onRequestedTargetHandled, requestedOpenVersion, requestedTarget, requestedTargetKey, resetForTargetLoad, requestedSessionId, sessionLoadReady]);
1104
+ useEffect(() => {
1105
+ if (!activeTarget)
1106
+ return;
1107
+ onContextChange?.({ target: activeTarget, sessionId: selectedSessionId });
1108
+ }, [activeTarget, onContextChange, selectedSessionId]);
1109
+ useEffect(() => {
1110
+ if (!activeTarget || typeof document === 'undefined' || !sessionLoadReady)
1111
+ return undefined;
1112
+ const reloadIfIdle = () => {
1113
+ if (Date.now() < suppressVisibilityReloadUntilRef.current)
1114
+ return;
1115
+ if (currentDraftSnapshotRef.current !== lastSavedSnapshotRef.current || saving || loading)
1116
+ return;
1117
+ void loadSessions(activeTarget, { requestedSessionId: selectedSessionId });
1118
+ };
1119
+ const handleVisibilityChange = () => {
1120
+ if (document.visibilityState !== 'visible')
1121
+ return;
1122
+ reloadIfIdle();
1123
+ };
1124
+ document.addEventListener('visibilitychange', handleVisibilityChange);
1125
+ return () => {
1126
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
1127
+ };
1128
+ }, [activeTarget, loadSessions, loading, saving, selectedSessionId, sessionLoadReady]);
1129
+ useEffect(() => {
1130
+ if (!activeTarget?.path) {
1131
+ setImageLoading(false);
1132
+ setImageLoadError(false);
1133
+ setNaturalImageSize({ width: 0, height: 0 });
1134
+ return;
1135
+ }
1136
+ setImageLoading(true);
1137
+ setImageLoadError(false);
1138
+ setNaturalImageSize({ width: 0, height: 0 });
1139
+ setZoomLevel(1);
1140
+ setPanMode(false);
1141
+ }, [activeTarget?.path]);
1142
+ useEffect(() => {
1143
+ if (!imageLoading)
1144
+ return;
1145
+ const image = imageRef.current;
1146
+ if (!image || !image.complete || image.naturalWidth <= 0 || image.naturalHeight <= 0)
1147
+ return;
1148
+ setNaturalImageSize({
1149
+ width: image.naturalWidth,
1150
+ height: image.naturalHeight,
1151
+ });
1152
+ setImageLoading(false);
1153
+ setImageLoadError(false);
1154
+ }, [activeTarget?.path, imageLoading]);
1155
+ useEffect(() => {
1156
+ if (canPan)
1157
+ return;
1158
+ setPanMode(false);
1159
+ }, [canPan]);
1160
+ useEffect(() => {
1161
+ setPayloadPreview(null);
1162
+ }, [selectedSessionId]);
1163
+ useEffect(() => {
1164
+ currentDraftSnapshotRef.current = currentDraftSnapshot;
1165
+ if (suppressAutoSaveRef.current) {
1166
+ suppressAutoSaveRef.current = false;
1167
+ return;
1168
+ }
1169
+ if (!selectedSessionId) {
1170
+ clearPendingAutoSave();
1171
+ setLastSaveError(null);
1172
+ setSaveState('saved');
1173
+ return;
1174
+ }
1175
+ if (currentDraftSnapshot === lastSavedSnapshotRef.current) {
1176
+ clearPendingAutoSave();
1177
+ if (!inFlightPromiseRef.current) {
1178
+ setLastSaveError(null);
1179
+ setSaveState('saved');
1180
+ }
1181
+ return;
1182
+ }
1183
+ const pendingDelayMs = pendingDebounceMsRef.current;
1184
+ pendingDebounceMsRef.current = null;
1185
+ if (saveState === 'error' && pendingDelayMs === null) {
1186
+ return;
1187
+ }
1188
+ const delayMs = pendingDelayMs ?? 600;
1189
+ if (inFlightPromiseRef.current) {
1190
+ hasPendingSaveAfterFlightRef.current = true;
1191
+ setSaveState('pending');
1192
+ return;
1193
+ }
1194
+ scheduleAutoSave(delayMs);
1195
+ }, [clearPendingAutoSave, currentDraftSnapshot, saveState, scheduleAutoSave, selectedSessionId]);
1196
+ useEffect(() => {
1197
+ if (!selectedSessionId)
1198
+ return;
1199
+ if (inFlightPromiseRef.current)
1200
+ return;
1201
+ if (currentDraftSnapshot !== lastSavedSnapshotRef.current)
1202
+ return;
1203
+ if (saveState === 'error')
1204
+ return;
1205
+ if (saveState === 'saved')
1206
+ return;
1207
+ setLastSaveError(null);
1208
+ setSaveState('saved');
1209
+ }, [currentDraftSnapshot, saveState, selectedSessionId]);
1210
+ useEffect(() => {
1211
+ if (typeof window === 'undefined')
1212
+ return undefined;
1213
+ const handleBeforeUnload = (event) => {
1214
+ if (currentDraftSnapshotRef.current === lastSavedSnapshotRef.current)
1215
+ return;
1216
+ event.preventDefault();
1217
+ event.returnValue = '';
1218
+ };
1219
+ window.addEventListener('beforeunload', handleBeforeUnload);
1220
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
1221
+ }, []);
1222
+ useEffect(() => {
1223
+ if (typeof document === 'undefined')
1224
+ return undefined;
1225
+ const handleVisibilityChange = () => {
1226
+ if (document.visibilityState !== 'hidden')
1227
+ return;
1228
+ void flushPendingDraft('visibility-hidden');
1229
+ };
1230
+ document.addEventListener('visibilitychange', handleVisibilityChange);
1231
+ return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
1232
+ }, [flushPendingDraft]);
1233
+ useEffect(() => {
1234
+ if (!colorPickerOpen || typeof document === 'undefined')
1235
+ return undefined;
1236
+ const handlePointerDown = (event) => {
1237
+ const target = event.target;
1238
+ if (!(target instanceof Node))
1239
+ return;
1240
+ if (colorPickerRef.current?.contains(target))
1241
+ return;
1242
+ setColorPickerOpen(false);
1243
+ };
1244
+ document.addEventListener('mousedown', handlePointerDown);
1245
+ return () => document.removeEventListener('mousedown', handlePointerDown);
1246
+ }, [colorPickerOpen]);
1247
+ useEffect(() => {
1248
+ if (!sessionCardEditing || typeof document === 'undefined')
1249
+ return undefined;
1250
+ const handlePointerDown = (event) => {
1251
+ const target = event.target;
1252
+ if (!(target instanceof Node))
1253
+ return;
1254
+ if (sessionCardRef.current?.contains(target))
1255
+ return;
1256
+ setSessionCardEditing(false);
1257
+ };
1258
+ document.addEventListener('mousedown', handlePointerDown);
1259
+ return () => document.removeEventListener('mousedown', handlePointerDown);
1260
+ }, [sessionCardEditing]);
1261
+ useEffect(() => () => {
1262
+ clearPendingAutoSave();
1263
+ }, [clearPendingAutoSave]);
1264
+ useEffect(() => {
1265
+ if (!payloadCopied)
1266
+ return;
1267
+ const timeoutId = window.setTimeout(() => setPayloadCopied(false), 2000);
1268
+ return () => window.clearTimeout(timeoutId);
1269
+ }, [payloadCopied]);
1270
+ useEffect(() => {
1271
+ if (!taskIdCopied)
1272
+ return;
1273
+ const timeoutId = window.setTimeout(() => setTaskIdCopied(false), 2000);
1274
+ return () => window.clearTimeout(timeoutId);
1275
+ }, [taskIdCopied]);
1276
+ useEffect(() => {
1277
+ if (!imageIdCopied)
1278
+ return;
1279
+ const timeoutId = window.setTimeout(() => setImageIdCopied(false), 2000);
1280
+ return () => window.clearTimeout(timeoutId);
1281
+ }, [imageIdCopied]);
1282
+ const updateDraftAnnotation = useCallback((annotationId, updater, delayMs = 300) => {
1283
+ setDraftAnnotations((current) => normalizeOrder(current.map((annotation) => (annotation.id === annotationId
1284
+ ? updater(annotation)
1285
+ : annotation))));
1286
+ markDraftChanged(delayMs);
1287
+ }, [markDraftChanged]);
1288
+ const applyMarkerColor = useCallback((color) => {
1289
+ if (!selectedAnnotation)
1290
+ return;
1291
+ updateDraftAnnotation(selectedAnnotation.id, (annotation) => ({
1292
+ ...annotation,
1293
+ color,
1294
+ }));
1295
+ setDefaultMarkerColor(color);
1296
+ setColorPickerOpen(false);
1297
+ }, [selectedAnnotation, updateDraftAnnotation]);
1298
+ const createSession = useCallback(async () => {
1299
+ if (!activeTarget)
1300
+ return;
1301
+ const flushed = await flushPendingDraft('session-switch');
1302
+ if (!flushed)
1303
+ return;
1304
+ setSaving(true);
1305
+ setError(null);
1306
+ try {
1307
+ const session = await createSessionForTarget(activeTarget);
1308
+ setShowCreateSessionPrompt(false);
1309
+ await loadSessions(activeTarget, { requestedSessionId: session.id, autoCreateIfEmpty: false });
1310
+ setToolMode('select');
1311
+ }
1312
+ catch (requestError) {
1313
+ setError(requestError instanceof Error ? requestError.message : 'Failed to create session.');
1314
+ setShowCreateSessionPrompt(true);
1315
+ }
1316
+ finally {
1317
+ setSaving(false);
1318
+ }
1319
+ }, [activeTarget, createSessionForTarget, flushPendingDraft, loadSessions]);
1320
+ const handleToolSelection = useCallback((toolValue) => {
1321
+ if (toolValue !== 'select' && !selectedSession) {
1322
+ setPanMode(false);
1323
+ setError('No session exists for this image yet.');
1324
+ setShowCreateSessionPrompt(true);
1325
+ return;
1326
+ }
1327
+ setError(null);
1328
+ setShowCreateSessionPrompt(false);
1329
+ setPanMode(false);
1330
+ setToolMode(toolValue);
1331
+ }, [selectedSession]);
1332
+ const duplicateSelectedSession = useCallback(async () => {
1333
+ if (!activeTarget || !selectedSession)
1334
+ return;
1335
+ const flushed = await flushPendingDraft('duplicate');
1336
+ if (!flushed)
1337
+ return;
1338
+ setSaving(true);
1339
+ setError(null);
1340
+ try {
1341
+ const clonedAnnotations = normalizeOrder(draftAnnotations.map((annotation, index) => cloneAnnotation(annotation, index)));
1342
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${workspaceQuery}`, {
1343
+ method: 'POST',
1344
+ credentials: 'include',
1345
+ headers: { 'Content-Type': 'application/json', ...requestHeaders },
1346
+ body: JSON.stringify({
1347
+ taskId: activeTarget.taskId,
1348
+ baseImageAssetId: activeTarget.assetId,
1349
+ title: buildDuplicateSessionTitle(draftTitle || selectedSession.title, activeTarget),
1350
+ globalInstruction: draftGlobalInstruction,
1351
+ annotations: clonedAnnotations,
1352
+ }),
1353
+ }, resolvedApiBase);
1354
+ const data = await res.json().catch(() => ({}));
1355
+ if (!res.ok) {
1356
+ throw new Error(String(data?.error || 'Failed to duplicate session.'));
1357
+ }
1358
+ const session = data?.session;
1359
+ if (!session?.id) {
1360
+ throw new Error('Failed to duplicate session.');
1361
+ }
1362
+ applySessionLocally(session);
1363
+ setToolMode('select');
1364
+ }
1365
+ catch (requestError) {
1366
+ setError(requestError instanceof Error ? requestError.message : 'Failed to duplicate session.');
1367
+ }
1368
+ finally {
1369
+ setSaving(false);
1370
+ }
1371
+ }, [
1372
+ activeTarget,
1373
+ draftAnnotations,
1374
+ draftGlobalInstruction,
1375
+ draftTitle,
1376
+ applySessionLocally,
1377
+ flushPendingDraft,
1378
+ requestHeaders,
1379
+ resolvedApiBase,
1380
+ selectedSession,
1381
+ workspaceQuery,
1382
+ ]);
1383
+ const saveSession = useCallback(async () => {
1384
+ return flushPendingDraft('manual');
1385
+ }, [flushPendingDraft]);
1386
+ const loadPayloadPreview = useCallback(async () => {
1387
+ if (!selectedSessionId)
1388
+ return;
1389
+ setPayloadLoading(true);
1390
+ setError(null);
1391
+ try {
1392
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(selectedSessionId)}/payload?${workspaceQuery}`, {
1393
+ credentials: 'include',
1394
+ headers: requestHeaders,
1395
+ }, resolvedApiBase);
1396
+ const data = await res.json().catch(() => ({}));
1397
+ if (!res.ok) {
1398
+ throw new Error(String(data?.error || 'Failed to load payload preview.'));
1399
+ }
1400
+ setPayloadPreview(data?.payload || null);
1401
+ }
1402
+ catch (requestError) {
1403
+ setError(requestError instanceof Error ? requestError.message : 'Failed to load payload preview.');
1404
+ }
1405
+ finally {
1406
+ setPayloadLoading(false);
1407
+ }
1408
+ }, [requestHeaders, resolvedApiBase, selectedSessionId, workspaceQuery]);
1409
+ const openPayloadPreview = useCallback(() => {
1410
+ if (!selectedSessionId)
1411
+ return;
1412
+ void flushPendingDraft('payload-preview').then((saved) => {
1413
+ if (!saved)
1414
+ return;
1415
+ setPayloadModalOpen(true);
1416
+ void loadPayloadPreview();
1417
+ });
1418
+ }, [flushPendingDraft, loadPayloadPreview, selectedSessionId]);
1419
+ const copyPayloadContent = useCallback(async (mode) => {
1420
+ if (!payloadPreview || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
1421
+ setError('Clipboard copy is not available in this browser.');
1422
+ return;
1423
+ }
1424
+ try {
1425
+ const text = mode === 'json'
1426
+ ? JSON.stringify(payloadPreview, null, 2)
1427
+ : buildPayloadBrief(payloadPreview);
1428
+ await navigator.clipboard.writeText(text);
1429
+ setPayloadCopied(mode);
1430
+ }
1431
+ catch (copyError) {
1432
+ setError(copyError instanceof Error ? copyError.message : 'Failed to copy payload content.');
1433
+ setPayloadCopied(false);
1434
+ }
1435
+ }, [payloadPreview]);
1436
+ const copyTaskId = useCallback(async () => {
1437
+ if (!activeTaskReferenceLabel || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
1438
+ setError('Clipboard copy is not available in this browser.');
1439
+ return;
1440
+ }
1441
+ try {
1442
+ await navigator.clipboard.writeText(activeTaskReferenceLabel);
1443
+ setTaskIdCopied(true);
1444
+ }
1445
+ catch (copyError) {
1446
+ setError(copyError instanceof Error ? copyError.message : 'Failed to copy task id.');
1447
+ setTaskIdCopied(false);
1448
+ }
1449
+ }, [activeTaskReferenceLabel]);
1450
+ const copyImageId = useCallback(async () => {
1451
+ if (!activeImageReferenceLabel || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
1452
+ setError('Clipboard copy is not available in this browser.');
1453
+ return;
1454
+ }
1455
+ try {
1456
+ await navigator.clipboard.writeText(activeImageReferenceLabel);
1457
+ setImageIdCopied(true);
1458
+ }
1459
+ catch (copyError) {
1460
+ setError(copyError instanceof Error ? copyError.message : 'Failed to copy image reference.');
1461
+ setImageIdCopied(false);
1462
+ }
1463
+ }, [activeImageReferenceLabel]);
1464
+ const deleteSelectedSession = useCallback(async () => {
1465
+ if (!selectedSessionId)
1466
+ return;
1467
+ const flushed = await flushPendingDraft('delete-session');
1468
+ if (!flushed)
1469
+ return;
1470
+ const sessionTitle = (selectedSession?.title || 'Untitled session').trim() || 'Untitled session';
1471
+ if (!window.confirm(`Delete the annotated attachment session "${sessionTitle}"?`))
1472
+ return;
1473
+ setSaving(true);
1474
+ setError(null);
1475
+ try {
1476
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(selectedSessionId)}?${workspaceQuery}`, {
1477
+ method: 'DELETE',
1478
+ credentials: 'include',
1479
+ headers: requestHeaders,
1480
+ }, resolvedApiBase);
1481
+ const data = await res.json().catch(() => ({}));
1482
+ if (!res.ok) {
1483
+ throw new Error(String(data?.error || 'Failed to delete session.'));
1484
+ }
1485
+ removeSessionLocally(selectedSessionId);
1486
+ setToolMode('select');
1487
+ }
1488
+ catch (requestError) {
1489
+ setError(requestError instanceof Error ? requestError.message : 'Failed to delete session.');
1490
+ }
1491
+ finally {
1492
+ setSaving(false);
1493
+ }
1494
+ }, [flushPendingDraft, removeSessionLocally, requestHeaders, resolvedApiBase, selectedSession, selectedSessionId, workspaceQuery]);
1495
+ const deleteSelectedAnnotation = useCallback(() => {
1496
+ if (!selectedAnnotationId)
1497
+ return;
1498
+ setDraftAnnotations((current) => normalizeOrder(current.filter((annotation) => annotation.id !== selectedAnnotationId)));
1499
+ setSelectedAnnotationId(null);
1500
+ markDraftChanged(300);
1501
+ }, [markDraftChanged, selectedAnnotationId]);
1502
+ const applyZoom = useCallback((nextZoomValue) => {
1503
+ const nextZoom = clampZoom(nextZoomValue);
1504
+ const scroller = canvasScrollerRef.current;
1505
+ if (!scroller || nextZoom === zoomLevel) {
1506
+ setZoomLevel(nextZoom);
1507
+ return;
1508
+ }
1509
+ const nextScrollLeft = ((scroller.scrollLeft + scroller.clientWidth / 2) * (nextZoom / zoomLevel)) - scroller.clientWidth / 2;
1510
+ const nextScrollTop = ((scroller.scrollTop + scroller.clientHeight / 2) * (nextZoom / zoomLevel)) - scroller.clientHeight / 2;
1511
+ setZoomLevel(nextZoom);
1512
+ window.requestAnimationFrame(() => {
1513
+ scroller.scrollLeft = Math.max(0, nextScrollLeft);
1514
+ scroller.scrollTop = Math.max(0, nextScrollTop);
1515
+ });
1516
+ }, [zoomLevel]);
1517
+ const zoomIn = useCallback(() => {
1518
+ applyZoom(zoomLevel + 0.25);
1519
+ }, [applyZoom, zoomLevel]);
1520
+ const zoomOut = useCallback(() => {
1521
+ applyZoom(zoomLevel - 0.25);
1522
+ }, [applyZoom, zoomLevel]);
1523
+ const resetZoom = useCallback(() => {
1524
+ applyZoom(1);
1525
+ const scroller = canvasScrollerRef.current;
1526
+ if (!scroller)
1527
+ return;
1528
+ window.requestAnimationFrame(() => {
1529
+ scroller.scrollLeft = 0;
1530
+ scroller.scrollTop = 0;
1531
+ });
1532
+ }, [applyZoom]);
1533
+ const moveSelectedAnnotation = useCallback((direction) => {
1534
+ if (!selectedAnnotationId)
1535
+ return;
1536
+ setDraftAnnotations((current) => {
1537
+ const fromIndex = current.findIndex((annotation) => annotation.id === selectedAnnotationId);
1538
+ if (fromIndex === -1)
1539
+ return current;
1540
+ const toIndex = direction === 'up' ? fromIndex - 1 : fromIndex + 1;
1541
+ if (toIndex < 0 || toIndex >= current.length)
1542
+ return current;
1543
+ markDraftChanged(300);
1544
+ return moveAnnotation(current, fromIndex, toIndex);
1545
+ });
1546
+ }, [markDraftChanged, selectedAnnotationId]);
1547
+ useEffect(() => {
1548
+ if (!selectedAnnotationId)
1549
+ return;
1550
+ const handleKeyDown = (event) => {
1551
+ if (event.key !== 'Delete')
1552
+ return;
1553
+ if (isEditableKeyboardTarget(event.target))
1554
+ return;
1555
+ event.preventDefault();
1556
+ deleteSelectedAnnotation();
1557
+ };
1558
+ window.addEventListener('keydown', handleKeyDown);
1559
+ return () => window.removeEventListener('keydown', handleKeyDown);
1560
+ }, [deleteSelectedAnnotation, selectedAnnotationId]);
1561
+ const readPoint = useCallback((event) => {
1562
+ const rect = overlayRef.current?.getBoundingClientRect();
1563
+ if (!rect || rect.width <= 0 || rect.height <= 0)
1564
+ return null;
1565
+ return {
1566
+ x: clamp01((event.clientX - rect.left) / rect.width),
1567
+ y: clamp01((event.clientY - rect.top) / rect.height),
1568
+ };
1569
+ }, []);
1570
+ const handleCanvasPointerDown = useCallback((event) => {
1571
+ if (panMode && canPan) {
1572
+ const scroller = canvasScrollerRef.current;
1573
+ if (!scroller)
1574
+ return;
1575
+ panRef.current = {
1576
+ pointerId: event.pointerId,
1577
+ startX: event.clientX,
1578
+ startY: event.clientY,
1579
+ scrollLeft: scroller.scrollLeft,
1580
+ scrollTop: scroller.scrollTop,
1581
+ };
1582
+ event.currentTarget.setPointerCapture(event.pointerId);
1583
+ event.preventDefault();
1584
+ return;
1585
+ }
1586
+ if (!selectedSessionId)
1587
+ return;
1588
+ if (toolMode === 'select') {
1589
+ setSelectedAnnotationId(null);
1590
+ return;
1591
+ }
1592
+ const point = readPoint(event);
1593
+ if (!point)
1594
+ return;
1595
+ if (toolMode === 'pin' || toolMode === 'text-note') {
1596
+ const annotation = createAnnotation(toolMode, point, point, defaultMarkerColor);
1597
+ setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
1598
+ setSelectedAnnotationId(annotation.id);
1599
+ markDraftChanged(300);
1600
+ setToolMode('select');
1601
+ return;
1602
+ }
1603
+ gestureStartRef.current = point;
1604
+ const annotation = createAnnotation(toolMode, point, point, defaultMarkerColor);
1605
+ creatingAnnotationRef.current = {
1606
+ annotationId: annotation.id,
1607
+ kind: toolMode,
1608
+ };
1609
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1610
+ setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
1611
+ setSelectedAnnotationId(annotation.id);
1612
+ markDraftChanged(300);
1613
+ }, [canPan, defaultMarkerColor, markDraftChanged, panMode, readPoint, selectedSessionId, toolMode]);
1614
+ const handleCanvasPointerUp = useCallback((event) => {
1615
+ if (panRef.current?.pointerId === event.pointerId) {
1616
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
1617
+ return;
1618
+ }
1619
+ if (!gestureStartRef.current || !creatingAnnotationRef.current || (toolMode !== 'box' && toolMode !== 'arrow'))
1620
+ return;
1621
+ const end = readPoint(event);
1622
+ gestureStartRef.current = null;
1623
+ creatingAnnotationRef.current = null;
1624
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
1625
+ if (!end)
1626
+ return;
1627
+ setToolMode('select');
1628
+ }, [readPoint, toolMode]);
1629
+ const startDragAnnotation = useCallback((event, annotation) => {
1630
+ if (toolMode !== 'select' || panMode)
1631
+ return;
1632
+ const point = readPoint(event);
1633
+ if (!point)
1634
+ return;
1635
+ dragRef.current = {
1636
+ annotationId: annotation.id,
1637
+ originPointer: point,
1638
+ originAnnotation: annotation,
1639
+ };
1640
+ setSelectedAnnotationId(annotation.id);
1641
+ event.stopPropagation();
1642
+ }, [panMode, readPoint, toolMode]);
1643
+ const handleCanvasPointerMove = useCallback((event) => {
1644
+ if (panRef.current?.pointerId === event.pointerId) {
1645
+ const scroller = canvasScrollerRef.current;
1646
+ if (!scroller)
1647
+ return;
1648
+ const dx = event.clientX - panRef.current.startX;
1649
+ const dy = event.clientY - panRef.current.startY;
1650
+ scroller.scrollLeft = panRef.current.scrollLeft - dx;
1651
+ scroller.scrollTop = panRef.current.scrollTop - dy;
1652
+ return;
1653
+ }
1654
+ if (creatingAnnotationRef.current && gestureStartRef.current) {
1655
+ const point = readPoint(event);
1656
+ if (!point)
1657
+ return;
1658
+ const { annotationId, kind } = creatingAnnotationRef.current;
1659
+ const start = gestureStartRef.current;
1660
+ updateDraftAnnotation(annotationId, (annotation) => {
1661
+ if (kind === 'box' && annotation.kind === 'box') {
1662
+ return {
1663
+ ...annotation,
1664
+ x: clamp01(Math.min(start.x, point.x)),
1665
+ y: clamp01(Math.min(start.y, point.y)),
1666
+ width: clampSize(Math.abs(point.x - start.x)),
1667
+ height: clampSize(Math.abs(point.y - start.y)),
1668
+ };
1669
+ }
1670
+ if (kind === 'arrow' && annotation.kind === 'arrow') {
1671
+ return {
1672
+ ...annotation,
1673
+ x: start.x,
1674
+ y: start.y,
1675
+ x2: point.x,
1676
+ y2: point.y,
1677
+ };
1678
+ }
1679
+ return annotation;
1680
+ });
1681
+ return;
1682
+ }
1683
+ if (resizeRef.current) {
1684
+ const point = readPoint(event);
1685
+ if (!point)
1686
+ return;
1687
+ const { annotationId, originPointer, originAnnotation } = resizeRef.current;
1688
+ const dx = point.x - originPointer.x;
1689
+ const dy = point.y - originPointer.y;
1690
+ updateDraftAnnotation(annotationId, () => ({
1691
+ ...originAnnotation,
1692
+ width: clampSize(originAnnotation.width + dx),
1693
+ height: clampSize(originAnnotation.height + dy),
1694
+ }));
1695
+ return;
1696
+ }
1697
+ if (arrowHandleRef.current) {
1698
+ const point = readPoint(event);
1699
+ if (!point)
1700
+ return;
1701
+ const { annotationId, endpoint, originPointer, originAnnotation } = arrowHandleRef.current;
1702
+ const dx = point.x - originPointer.x;
1703
+ const dy = point.y - originPointer.y;
1704
+ updateDraftAnnotation(annotationId, () => {
1705
+ if (endpoint === 'tail') {
1706
+ return {
1707
+ ...originAnnotation,
1708
+ x: clamp01(originAnnotation.x + dx),
1709
+ y: clamp01(originAnnotation.y + dy),
1710
+ };
1711
+ }
1712
+ return {
1713
+ ...originAnnotation,
1714
+ x2: clamp01(originAnnotation.x2 + dx),
1715
+ y2: clamp01(originAnnotation.y2 + dy),
1716
+ };
1717
+ });
1718
+ return;
1719
+ }
1720
+ if (!dragRef.current)
1721
+ return;
1722
+ const point = readPoint(event);
1723
+ if (!point)
1724
+ return;
1725
+ const { annotationId, originPointer, originAnnotation } = dragRef.current;
1726
+ const dx = point.x - originPointer.x;
1727
+ const dy = point.y - originPointer.y;
1728
+ updateDraftAnnotation(annotationId, () => {
1729
+ if (originAnnotation.kind === 'pin' || originAnnotation.kind === 'text-note') {
1730
+ return {
1731
+ ...originAnnotation,
1732
+ x: clamp01(originAnnotation.x + dx),
1733
+ y: clamp01(originAnnotation.y + dy),
1734
+ };
1735
+ }
1736
+ if (originAnnotation.kind === 'box') {
1737
+ return {
1738
+ ...originAnnotation,
1739
+ x: clamp01(originAnnotation.x + dx),
1740
+ y: clamp01(originAnnotation.y + dy),
1741
+ };
1742
+ }
1743
+ return {
1744
+ ...originAnnotation,
1745
+ x: clamp01(originAnnotation.x + dx),
1746
+ y: clamp01(originAnnotation.y + dy),
1747
+ x2: clamp01(originAnnotation.x2 + dx),
1748
+ y2: clamp01(originAnnotation.y2 + dy),
1749
+ };
1750
+ });
1751
+ }, [readPoint, updateDraftAnnotation]);
1752
+ const handleCanvasPointerLeave = useCallback(() => {
1753
+ gestureStartRef.current = null;
1754
+ dragRef.current = null;
1755
+ resizeRef.current = null;
1756
+ arrowHandleRef.current = null;
1757
+ creatingAnnotationRef.current = null;
1758
+ panRef.current = null;
1759
+ }, []);
1760
+ const handleCanvasPointerCancel = useCallback(() => {
1761
+ gestureStartRef.current = null;
1762
+ dragRef.current = null;
1763
+ resizeRef.current = null;
1764
+ arrowHandleRef.current = null;
1765
+ creatingAnnotationRef.current = null;
1766
+ panRef.current = null;
1767
+ }, []);
1768
+ const handleCanvasPointerEnd = useCallback(() => {
1769
+ dragRef.current = null;
1770
+ resizeRef.current = null;
1771
+ arrowHandleRef.current = null;
1772
+ creatingAnnotationRef.current = null;
1773
+ panRef.current = null;
1774
+ }, []);
1775
+ const startResizeBox = useCallback((event, annotation) => {
1776
+ if (toolMode !== 'select' || panMode)
1777
+ return;
1778
+ const point = readPoint(event);
1779
+ if (!point)
1780
+ return;
1781
+ resizeRef.current = {
1782
+ annotationId: annotation.id,
1783
+ originPointer: point,
1784
+ originAnnotation: annotation,
1785
+ };
1786
+ setSelectedAnnotationId(annotation.id);
1787
+ event.stopPropagation();
1788
+ }, [panMode, readPoint, toolMode]);
1789
+ const startArrowHandleDrag = useCallback((event, annotation, endpoint) => {
1790
+ if (toolMode !== 'select' || panMode)
1791
+ return;
1792
+ const point = readPoint(event);
1793
+ if (!point)
1794
+ return;
1795
+ arrowHandleRef.current = {
1796
+ annotationId: annotation.id,
1797
+ originPointer: point,
1798
+ originAnnotation: annotation,
1799
+ endpoint,
1800
+ };
1801
+ setSelectedAnnotationId(annotation.id);
1802
+ event.stopPropagation();
1803
+ }, [panMode, readPoint, toolMode]);
1804
+ const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
1805
+ return (_jsxs("div", { className: styles.shell, children: [_jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.sessionPanel}`, children: [_jsx("div", { className: styles.sessionContextBar, children: activeTarget?.taskId ? (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: onBackToTask ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.backToTaskBtn}`, onClick: () => {
1806
+ void flushPendingDraft('back-to-task').then((saved) => {
1807
+ if (!saved)
1808
+ return;
1809
+ if (activeTarget.taskId)
1810
+ onBackToTask(activeTarget.taskId);
1811
+ });
1812
+ }, "aria-label": "Back to task", title: "Back to task", children: _jsx(ArrowLeft, { size: 16 }) })) : _jsx("div", { className: styles.sessionContextSpacer, "aria-hidden": "true" }) }), _jsx("div", { className: styles.sessionContextRight, children: _jsx(TaskReferenceBadge, { copied: taskIdCopied, onClick: () => { void copyTaskId(); }, title: "Copy task id", ariaLabel: taskIdCopied ? 'Copied task id' : 'Copy task id', label: activeTaskReferenceLabel }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: _jsx("span", { className: `tf-label-micro ${styles.sessionContextLabel}`, children: "Unattached Image" }) }), _jsx("div", { className: styles.sessionContextRight, children: _jsx("div", { className: styles.sessionContextSpacer, "aria-hidden": "true" }) })] })) }), _jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Sessions" }), !activeTarget ? (_jsx("div", { className: "tf-text-secondary", children: "Open an image attachment to begin" })) : null] }), _jsx("div", { className: styles.sessionActions, children: _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => void createSession(), disabled: !activeTarget || saving, "aria-label": "Create session", title: "Create session", children: _jsx(Plus, { size: 16 }) }) })] }), !activeTarget ? (_jsxs("div", { className: styles.emptyState, children: [_jsx("strong", { className: "tf-empty-title", children: "No image selected" }), _jsx("p", { className: "tf-empty-copy", children: "Open an image attachment from a task to start an annotated session." })] })) : loading ? (_jsx("div", { className: styles.emptyState, children: _jsx("p", { className: "tf-empty-copy", children: "Loading sessions\u2026" }) })) : (_jsx("div", { className: `tf-scrollbar ${styles.sessionList}`, children: sessions.length === 0 ? (_jsxs("div", { className: styles.sessionEmptyState, children: [_jsx("div", { className: `tf-heading-card ${styles.sessionTitle}`, children: "No sessions yet" }), _jsx("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: "Create the first annotation session for this image." })] })) : sessions.map((session) => {
1813
+ const isSelected = selectedSessionId === session.id;
1814
+ const isEditingSelectedSession = isSelected && sessionCardEditing;
1815
+ const preview = sessionInstructionPreview(isSelected ? draftGlobalInstruction : session.globalInstruction);
1816
+ const sessionTitle = (isSelected ? draftTitle : session.title) || 'Untitled session';
1817
+ return (_jsxs("div", { className: `tf-surface-elevated ${styles.sessionCard} ${isSelected ? styles.sessionCardActive : ''}`, ref: isSelected ? sessionCardRef : undefined, onBlur: isEditingSelectedSession
1818
+ ? (event) => {
1819
+ const nextTarget = event.relatedTarget;
1820
+ if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget))
1821
+ return;
1822
+ setSessionCardEditing(false);
1823
+ }
1824
+ : undefined, children: [isEditingSelectedSession ? (_jsxs("div", { className: styles.sessionCardBody, children: [_jsxs("div", { className: styles.sessionMeta, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-session-title", children: "Session title" }), _jsx("input", { id: "annotated-session-title", className: `tf-field-shell ${styles.textInput} ${styles.sessionTitleInput}`, value: draftTitle, onChange: (event) => {
1825
+ setDraftTitle(event.target.value);
1826
+ markDraftChanged(600);
1827
+ }, placeholder: "Session title" }), _jsx("span", { className: `tf-text-meta ${styles.sessionTimestamp}`, children: formatTimestamp(session.updatedAt) })] }), getSessionCreatorLabel(session) ? (_jsxs("div", { className: `tf-text-secondary ${styles.sessionCreator}`, children: ["Created by ", getSessionCreatorLabel(session)] })) : null, _jsxs("div", { className: styles.sessionInstructionEditor, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-session-instruction", children: "Session instruction" }), _jsx("textarea", { id: "annotated-session-instruction", className: `tf-field-shell ${styles.textArea} ${styles.sessionInstructionField}`, value: draftGlobalInstruction, onChange: (event) => {
1828
+ setDraftGlobalInstruction(event.target.value);
1829
+ markDraftChanged(600);
1830
+ }, placeholder: "Add overall instructions, context, or framing for this session." })] }), _jsxs("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: [session.annotations.length, " annotation", session.annotations.length === 1 ? '' : 's'] })] })) : (_jsxs("button", { type: "button", className: styles.sessionCardButton, onClick: () => {
1831
+ if (isSelected) {
1832
+ setSessionCardEditing(true);
1833
+ return;
1834
+ }
1835
+ void flushPendingDraft('session-switch').then((saved) => {
1836
+ if (!saved)
1837
+ return;
1838
+ setSelectedSessionId(session.id);
1839
+ syncDraftFromSession(session, activeTarget);
1840
+ });
1841
+ }, "aria-pressed": isSelected, children: [_jsxs("div", { className: styles.sessionMeta, children: [_jsx("span", { className: `tf-heading-card ${styles.sessionTitle}`, children: sessionTitle }), _jsx("span", { className: `tf-text-meta ${styles.sessionTimestamp}`, children: formatTimestamp(session.updatedAt) })] }), getSessionCreatorLabel(session) ? (_jsxs("div", { className: `tf-text-secondary ${styles.sessionCreator}`, children: ["Created by ", getSessionCreatorLabel(session)] })) : null, preview ? (_jsx("div", { className: `tf-text-secondary ${styles.sessionInstructionPreview}`, children: preview })) : null, _jsxs("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: [session.annotations.length, " annotation", session.annotations.length === 1 ? '' : 's'] })] })), isSelected ? (_jsx("div", { className: styles.sessionCardFooter, children: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: () => void duplicateSelectedSession(), disabled: saving, "aria-label": "Duplicate session", title: `Duplicate session "${sessionTitle}"`, children: _jsx(Copy, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: () => void deleteSelectedSession(), disabled: saving, "aria-label": "Delete session", title: `Delete session "${sessionTitle}"`, children: _jsx(Trash2, { size: 16 }) })] }) })) : null] }, session.id));
1842
+ }) }))] }), _jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.canvasPanel}`, children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.canvasHeading, children: [_jsx("div", { className: "tf-heading-card", children: activeTarget?.displayName || 'Annotated attachment' }), _jsx("div", { className: "tf-text-secondary", children: selectedSession ? `${draftAnnotations.length} markers · ${activeSessionTimestamp}` : 'Pick or create a session' })] }), activeImageReferenceLabel ? (_jsx(ImageReferenceBadge, { copied: imageIdCopied, onClick: () => { void copyImageId(); }, title: "Copy image reference", ariaLabel: imageIdCopied ? 'Copied image reference' : 'Copy image reference', label: activeImageReferenceLabel })) : null] }), _jsxs("div", { className: styles.toolRail, children: [_jsxs("div", { className: `${styles.toolbarCluster} ${styles.toolbarViewportCluster}`, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void saveSession(), disabled: !selectedSession || saving || (!hasUnsavedChanges && saveState !== 'error'), "aria-label": saving ? 'Saving session' : 'Save session', title: saving ? 'Saving session' : 'Save session', children: _jsx(Save, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => setOpenImageModalOpen(true), disabled: openingImage, "aria-label": "Open image", title: "Open image", children: _jsx(FolderOpen, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void pasteImageFromClipboard(), disabled: pastingImage, "aria-label": pastingImage ? 'Pasting image' : 'Paste image', title: pastingImage ? 'Pasting image' : 'Paste image', children: pastingImage ? _jsx(Loader2, { size: 18, className: sharedStyles.spin }) : _jsx(ClipboardPaste, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => {
1843
+ if (!selectedSession)
1844
+ return;
1845
+ restoreLastSavedDraft();
1846
+ }, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${panMode ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
1847
+ const nextValue = parsePercentField(event.target.value);
1848
+ if (nextValue === null)
1849
+ return;
1850
+ updateDraftAnnotation(selectedAnnotation.id, (annotation) => (updateAnnotationGeometryField(annotation, field.key, nextValue)));
1851
+ }, "aria-label": `${field.label} percent` })] }, field.key))) })] })) : null }), _jsxs("div", { className: styles.toolbarUtilities, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.payloadBtn} ${styles.toolbarButton}`, onClick: openPayloadPreview, disabled: !selectedSession || payloadLoading, "aria-label": payloadLoading ? 'Loading payload preview' : 'Preview payload', title: payloadLoading ? 'Loading payload preview' : 'Preview payload', children: _jsx(MessageSquareCode, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => setToolHelpModalOpen(true), "aria-label": "How to use marker tools", title: "How to use marker tools", children: _jsx(HelpCircle, { size: 18 }) })] })] })] }), _jsxs("div", { className: styles.canvasWorkspace, children: [_jsx("div", { className: styles.canvasToolRail, "aria-label": "Annotation tools", children: TOOL_OPTIONS.map((tool) => {
1852
+ const Icon = tool.icon;
1853
+ return (_jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${toolMode === tool.value ? styles.toolBtnActive : ''}`, onClick: () => handleToolSelection(tool.value), disabled: !activeTarget, title: tool.label, "aria-label": `${tool.label} tool. ${TOOL_HELP_COPY[tool.value].short}`, children: _jsx(Icon, { size: 18 }) }, tool.value));
1854
+ }) }), _jsx("div", { className: styles.canvasMain, children: _jsx("div", { ref: canvasScrollerRef, className: `tf-scrollbar ${styles.canvasScroller}`, children: !activeTarget ? (_jsx("div", { className: styles.emptyState, children: _jsx("p", { className: "tf-empty-copy", children: "Select an image attachment to annotate." }) })) : (_jsxs(_Fragment, { children: [imageLoading && !imageLoadError ? (_jsx("div", { className: styles.canvasStatusOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.canvasStatusCard, children: [_jsx(Loader2, { size: 20, className: sharedStyles.spinner }), _jsx("span", { children: "Loading image\u2026" })] }) })) : null, imageLoadError ? (_jsx("div", { className: styles.canvasStatusOverlay, "aria-live": "polite", children: _jsx("div", { className: styles.canvasStatusCard, children: _jsx("span", { children: "Image failed to load." }) }) })) : null, _jsx("div", { className: styles.canvasFrame, children: _jsxs("div", { className: styles.canvasMedia, style: {
1855
+ width: canvasMediaSize.width ? `${canvasMediaSize.width}px` : undefined,
1856
+ height: canvasMediaSize.height ? `${canvasMediaSize.height}px` : undefined,
1857
+ }, children: [_jsx("img", { ref: imageRef, src: activeTarget.path, alt: activeTarget.displayName, className: styles.canvasImage, onLoad: () => {
1858
+ const image = imageRef.current;
1859
+ setNaturalImageSize({
1860
+ width: image?.naturalWidth || 0,
1861
+ height: image?.naturalHeight || 0,
1862
+ });
1863
+ setImageLoading(false);
1864
+ setImageLoadError(false);
1865
+ }, onError: () => {
1866
+ setNaturalImageSize({ width: 0, height: 0 });
1867
+ setImageLoading(false);
1868
+ setImageLoadError(true);
1869
+ } }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${panMode && canPan ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
1870
+ handleCanvasPointerUp(event);
1871
+ handleCanvasPointerEnd();
1872
+ }, "data-testid": "annotated-attachment-overlay", onPointerLeave: handleCanvasPointerLeave, onPointerCancel: handleCanvasPointerCancel, children: [_jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", "aria-hidden": "true", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => {
1873
+ const geometry = getArrowGeometry(annotation, canvasMediaSize);
1874
+ const color = mapMarkerColor(annotation);
1875
+ const isSelected = selectedAnnotationId === annotation.id;
1876
+ return (_jsxs(React.Fragment, { children: [isSelected ? (_jsxs(_Fragment, { children: [_jsx("line", { x1: geometry.shaftX1, y1: geometry.shaftY1, x2: geometry.shaftX2, y2: geometry.shaftY2, stroke: "color-mix(in srgb, var(--surface-elevated) 92%, transparent)", strokeWidth: 14, strokeLinecap: "round" }), _jsx("polygon", { points: geometry.headPoints, fill: color, stroke: "color-mix(in srgb, var(--surface-elevated) 92%, transparent)", strokeWidth: 4, strokeLinejoin: "round" })] })) : null, _jsx("line", { x1: geometry.shaftX1, y1: geometry.shaftY1, x2: geometry.shaftX2, y2: geometry.shaftY2, stroke: color, strokeWidth: isSelected ? 10 : 6, strokeLinecap: "round" }), _jsx("polygon", { points: geometry.headPoints, fill: color })] }, annotation.id));
1877
+ }) }), _jsx("svg", { className: styles.overlayHitLayer, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => (_jsxs(React.Fragment, { children: [_jsx("line", { x1: annotation.x * canvasMediaSize.width, y1: annotation.y * canvasMediaSize.height, x2: annotation.x2 * canvasMediaSize.width, y2: annotation.y2 * canvasMediaSize.height, className: styles.arrowHitArea, "data-testid": `annotated-arrow-hit-${annotation.id}`, "aria-label": `Arrow marker ${annotationOrderNumbers.get(annotation.id) || 0}`, onPointerDown: (event) => startDragAnnotation(event, annotation), onClick: (event) => {
1878
+ event.stopPropagation();
1879
+ setSelectedAnnotationId(annotation.id);
1880
+ } }), selectedAnnotationId === annotation.id ? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: annotation.x * canvasMediaSize.width, cy: annotation.y * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: styles.canvasHandleHit, role: "button", tabIndex: 0, "aria-label": "Move arrow tail", onPointerDown: (event) => startArrowHandleDrag(event, annotation, 'tail') }), _jsx("circle", { cx: annotation.x * canvasMediaSize.width, cy: annotation.y * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" }), _jsx("circle", { cx: annotation.x2 * canvasMediaSize.width, cy: annotation.y2 * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: styles.canvasHandleHit, role: "button", tabIndex: 0, "aria-label": "Move arrow tip", onPointerDown: (event) => startArrowHandleDrag(event, annotation, 'tip') }), _jsx("circle", { cx: annotation.x2 * canvasMediaSize.width, cy: annotation.y2 * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" })] })) : null] }, `hit-${annotation.id}`))) }), draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => (_jsx("div", { className: `${styles.annotationNumberBadge} ${styles.arrowNumberBadge}`, style: {
1881
+ left: `${((annotation.x + annotation.x2) / 2) * 100}%`,
1882
+ top: `${((annotation.y + annotation.y2) / 2) * 100}%`,
1883
+ backgroundColor: mapMarkerColor(annotation),
1884
+ }, "aria-hidden": "true", children: annotationOrderNumbers.get(annotation.id) || 0 }, `arrow-number-${annotation.id}`))), draftAnnotations.filter((annotation) => annotation.kind !== 'arrow').map((annotation) => {
1885
+ const color = mapMarkerColor(annotation);
1886
+ const annotationOrderNumber = annotationOrderNumbers.get(annotation.id) || 0;
1887
+ const sharedProps = {
1888
+ onPointerDown: (event) => startDragAnnotation(event, annotation),
1889
+ onClick: (event) => {
1890
+ event.stopPropagation();
1891
+ setSelectedAnnotationId(annotation.id);
1892
+ },
1893
+ };
1894
+ if (annotation.kind === 'pin') {
1895
+ return (_jsx("button", { type: "button", ...sharedProps, className: `${styles.pin} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: { left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%`, backgroundColor: color }, children: annotationOrderNumber }, annotation.id));
1896
+ }
1897
+ if (annotation.kind === 'text-note') {
1898
+ return (_jsx("button", { type: "button", ...sharedProps, className: `${styles.note} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: { left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%`, backgroundColor: color }, children: annotationOrderNumber }, annotation.id));
1899
+ }
1900
+ return (_jsxs("div", { className: `${styles.box} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: {
1901
+ left: `${annotation.x * 100}%`,
1902
+ top: `${annotation.y * 100}%`,
1903
+ width: `${annotation.width * 100}%`,
1904
+ height: `${annotation.height * 100}%`,
1905
+ borderColor: color,
1906
+ }, children: [_jsx("span", { className: `${styles.annotationNumberBadge} ${styles.boxNumberBadge}`, style: { backgroundColor: color }, "aria-hidden": "true", children: annotationOrderNumber }), _jsx("button", { type: "button", ...sharedProps, className: styles.boxSurface, "aria-label": `Box marker ${annotationOrderNumber}` })] }, annotation.id));
1907
+ }), _jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", children: draftAnnotations.filter((annotation) => annotation.kind === 'box' && selectedAnnotationId === annotation.id).map((annotation) => (_jsxs(React.Fragment, { children: [_jsx("circle", { cx: (annotation.x + annotation.width) * canvasMediaSize.width, cy: (annotation.y + annotation.height) * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: `${styles.canvasHandleHit} ${styles.canvasResizeHandleHit}`, role: "button", tabIndex: 0, "aria-label": "Resize box marker", onPointerDown: (event) => startResizeBox(event, annotation) }), _jsx("circle", { cx: (annotation.x + annotation.width) * canvasMediaSize.width, cy: (annotation.y + annotation.height) * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" })] }, `box-handle-${annotation.id}`))) })] })) : null] }) })] })) }) })] }), _jsxs("div", { className: styles.statusBar, children: [_jsx("span", { children: saveState === 'error'
1908
+ ? 'Save failed. Retry now.'
1909
+ : saveState === 'saving' || saveState === 'pending'
1910
+ ? 'Saving…'
1911
+ : 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : panMode && canPan ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
1912
+ const AnnotationKindIcon = ANNOTATION_KIND_ICON[annotation.kind];
1913
+ const annotationKindLabel = ANNOTATION_KIND_LABELS[annotation.kind];
1914
+ const annotationSequenceLabel = `${annotationKindLabel} ${index + 1}`;
1915
+ const InstructionTypeIcon = MARKER_TYPE_ICON[annotation.markerType || DEFAULT_MARKER_TYPE];
1916
+ const instructionTypeLabel = MARKER_TYPE_OPTIONS.find((option) => option.value === (annotation.markerType || DEFAULT_MARKER_TYPE))?.label || 'Review';
1917
+ const isSelected = selectedAnnotationId === annotation.id;
1918
+ return (_jsxs("div", { className: `tf-surface-elevated ${styles.annotationCard} ${isSelected ? styles.annotationCardActive : ''}`, children: [_jsx("button", { type: "button", className: styles.annotationCardButton, onClick: () => setSelectedAnnotationId(annotation.id), "aria-label": annotationKindLabel, children: _jsxs("div", { className: styles.annotationMeta, children: [_jsx("span", { className: `tf-heading-card ${styles.annotationTitle}`, children: annotationSequenceLabel }), _jsx("span", { className: `tf-text-meta ${styles.annotationKind}`, "aria-hidden": "true", children: _jsx(AnnotationKindIcon, { size: 16 }) })] }) }), isSelected ? (_jsxs("div", { className: styles.annotationInstructionEditor, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-marker-instruction", children: "Marker instruction" }), _jsx("textarea", { id: "annotated-marker-instruction", ref: isSelected ? selectedInstructionTextareaRef : null, className: `tf-field-shell ${styles.textArea} ${styles.annotationInstructionField}`, value: annotation.instruction, onChange: (event) => {
1919
+ updateDraftAnnotation(annotation.id, (current) => ({
1920
+ ...current,
1921
+ instruction: event.target.value,
1922
+ }), 600);
1923
+ }, onBlur: () => {
1924
+ void flushPendingDraft('text');
1925
+ }, placeholder: "What should the AI focus on for this marker?" }), _jsxs("div", { className: styles.annotationTypeField, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-marker-type", children: "Instruction type" }), _jsx("select", { id: "annotated-marker-type", className: `tf-field-shell ${styles.select}`, value: annotation.markerType || DEFAULT_MARKER_TYPE, onChange: (event) => {
1926
+ updateDraftAnnotation(annotation.id, (current) => updateMarkerType(current, event.target.value));
1927
+ }, children: MARKER_TYPE_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] })] })) : (_jsx("button", { type: "button", className: styles.annotationInstructionButton, onClick: () => setSelectedAnnotationId(annotation.id), "aria-label": `Edit ${annotationKindLabel} instruction`, children: _jsxs("div", { className: styles.annotationInstructionEditor, children: [_jsx("div", { className: `tf-text-secondary ${styles.annotationInstructionPreview}`, children: annotation.instruction.trim() || 'No marker instruction yet.' }), _jsx("div", { className: styles.annotationPreviewFooter, children: _jsx("span", { className: `tf-text-meta ${styles.annotationInstructionTypeIcon}`, "aria-label": `Instruction type: ${instructionTypeLabel}`, title: instructionTypeLabel, children: _jsx(InstructionTypeIcon, { size: 16 }) }) })] }) }))] }, annotation.id));
1928
+ }) })] })) : (_jsxs("div", { className: styles.emptyState, children: [_jsx("strong", { className: "tf-empty-title", children: "No active session" }), _jsx("p", { className: "tf-empty-copy", children: "Create or select a session to edit annotations." })] })) }), _jsx(Modal, { isOpen: openImageModalOpen, onClose: () => {
1929
+ if (openingImage)
1930
+ return;
1931
+ setOpenImageModalOpen(false);
1932
+ setOpenImageReference('');
1933
+ }, title: "Open Image", size: "sm", children: _jsxs("form", { className: styles.openImageModalBody, onSubmit: (event) => {
1934
+ event.preventDefault();
1935
+ void openImageByReference();
1936
+ }, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-open-image-reference", children: "Image reference number" }), _jsx("input", { id: "annotated-open-image-reference", className: `tf-field-shell ${styles.textInput} ${styles.openImageField}`, value: openImageReference, onChange: (event) => setOpenImageReference(event.target.value), placeholder: "I-24", autoFocus: true }), _jsxs("div", { className: styles.openImageActions, children: [_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", onClick: () => {
1937
+ setOpenImageModalOpen(false);
1938
+ setOpenImageReference('');
1939
+ }, disabled: openingImage, children: "Cancel" }), _jsx("button", { type: "submit", className: "tf-button-primary tf-button-compact", disabled: openingImage, children: openingImage ? 'Opening…' : 'Open' })] })] }) }), _jsx(Modal, { isOpen: toolHelpModalOpen, onClose: () => setToolHelpModalOpen(false), title: "How To Use Markers", size: "md", children: _jsx("div", { className: styles.markerHelpModalBody, children: TOOL_OPTIONS.filter((tool) => tool.value !== 'select').map((tool) => (_jsxs("div", { className: `tf-surface-inset ${styles.markerHelpItem}`, children: [_jsxs("div", { className: styles.markerHelpHeader, children: [_jsx("strong", { className: "tf-heading-card", children: tool.label }), _jsx("span", { className: "tf-text-meta", children: TOOL_HELP_COPY[tool.value].short })] }), _jsx("p", { className: "tf-text-secondary", children: TOOL_HELP_COPY[tool.value].detail }), _jsx("p", { className: `tf-text-body ${styles.markerHelpExample}`, children: TOOL_HELP_COPY[tool.value].example })] }, tool.value))) }) }), _jsx(Modal, { isOpen: payloadModalOpen, onClose: () => setPayloadModalOpen(false), title: "AI Payload", size: "lg", children: _jsxs("div", { className: styles.payloadModalBody, children: [_jsxs("div", { className: styles.payloadModalToolbar, children: [_jsxs("div", { className: styles.payloadViewToggle, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${payloadPreviewMode === 'json' ? styles.toolBtnActive : ''}`, onClick: () => setPayloadPreviewMode('json'), children: "JSON" }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${payloadPreviewMode === 'brief' ? styles.toolBtnActive : ''}`, onClick: () => setPayloadPreviewMode('brief'), children: "AI Brief" })] }), _jsxs("div", { className: styles.payloadModalActions, children: [_jsxs("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn}`, onClick: () => void copyPayloadContent('json'), disabled: !payloadPreview, "aria-label": payloadCopied === 'json' ? 'Copied JSON' : 'Copy JSON', title: payloadCopied === 'json' ? 'Copied JSON' : 'Copy JSON', children: [payloadCopied === 'json' ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }), _jsx("span", { children: "JSON" })] }), _jsxs("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn}`, onClick: () => void copyPayloadContent('brief'), disabled: !payloadPreview, "aria-label": payloadCopied === 'brief' ? 'Copied AI Brief' : 'Copy AI Brief', title: payloadCopied === 'brief' ? 'Copied AI Brief' : 'Copy AI Brief', children: [payloadCopied === 'brief' ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }), _jsx("span", { children: "AI Brief" })] })] })] }), payloadLoading ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Loading payload\u2026" }) })) : payloadPreview ? (_jsx("pre", { className: `tf-surface-inset tf-scrollbar ${styles.payloadModalPreview}`, children: payloadPreviewMode === 'json'
1940
+ ? JSON.stringify(payloadPreview, null, 2)
1941
+ : buildPayloadBrief(payloadPreview) })) : (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Load a payload preview to inspect the current session contract." }) }))] }) })] }));
1942
+ }