@taskforcehq/taskforce 0.3.304 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -7,9 +7,15 @@ describe('AgentsModule', () => {
7
7
  vi.restoreAllMocks();
8
8
  });
9
9
  it('groups registered AI profiles by surface type in a stable section order', async () => {
10
- const fetchMock = vi.fn().mockResolvedValue({
10
+ const fetchMock = vi.fn()
11
+ .mockResolvedValueOnce({
11
12
  ok: true,
12
13
  json: async () => ({
14
+ aiProfileSeatUsage: {
15
+ used: 2,
16
+ limit: null,
17
+ remaining: null
18
+ },
13
19
  assignees: [
14
20
  { value: 'member-1', label: 'Member One', kind: 'member' },
15
21
  {
@@ -20,6 +26,7 @@ describe('AgentsModule', () => {
20
26
  color: '#10a37f',
21
27
  kind: 'agent',
22
28
  surfaceType: 'chat_app',
29
+ seatScope: 'cloud_metered',
23
30
  description: 'General-purpose chat surface for planning and writing.',
24
31
  role: 'Product copilot',
25
32
  provider: 'OpenAI',
@@ -36,6 +43,7 @@ describe('AgentsModule', () => {
36
43
  color: '#0ea5e9',
37
44
  kind: 'agent',
38
45
  surfaceType: 'coding_tool',
46
+ seatScope: 'local_unmetered',
39
47
  description: 'Terminal-first coding agent.',
40
48
  role: 'Implementation partner',
41
49
  provider: 'OpenAI',
@@ -52,6 +60,7 @@ describe('AgentsModule', () => {
52
60
  color: '#0ea5e9',
53
61
  kind: 'agent',
54
62
  surfaceType: 'coding_tool',
63
+ seatScope: 'local_unmetered',
55
64
  description: 'Duplicate Codex profile.',
56
65
  role: 'Implementation partner',
57
66
  provider: 'OpenAI',
@@ -68,6 +77,7 @@ describe('AgentsModule', () => {
68
77
  color: '#64748b',
69
78
  kind: 'agent',
70
79
  surfaceType: null,
80
+ seatScope: 'cloud_metered',
71
81
  createdAt: '2026-03-28T08:00:00.000Z',
72
82
  updatedAt: '2026-04-01T08:00:00.000Z',
73
83
  lastActiveAt: null
@@ -76,7 +86,7 @@ describe('AgentsModule', () => {
76
86
  })
77
87
  });
78
88
  vi.stubGlobal('fetch', fetchMock);
79
- const { container } = render(_jsx(AgentsModule, { workspaceId: "workspace-1" }));
89
+ const { container } = render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true, cloudAiProfileSeatUsage: { used: 2, limit: 5, remaining: 3 } }));
80
90
  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
81
91
  expect((await screen.findAllByText('Chat Apps')).length).toBeGreaterThan(0);
82
92
  expect(screen.getAllByText('Coding Tools').length).toBeGreaterThan(0);
@@ -84,19 +94,259 @@ describe('AgentsModule', () => {
84
94
  expect(screen.getAllByText('Codex')).toHaveLength(1);
85
95
  expect(screen.getByText(/2 duplicates/i)).toBeInTheDocument();
86
96
  expect(screen.getByText('Coding tool')).toBeInTheDocument();
97
+ expect(screen.getAllByText('Cloud MCP').length).toBeGreaterThan(0);
98
+ expect(screen.getAllByText('Local MCP').length).toBeGreaterThan(0);
99
+ expect(screen.getByText('Registered Cloud Agents: 2/5')).toBeInTheDocument();
87
100
  expect(screen.getAllByText('General-purpose chat surface for planning and writing.').length).toBeGreaterThan(0);
88
101
  expect(screen.getAllByText('Product copilot').length).toBeGreaterThan(0);
89
- expect(screen.getByText('OpenAI · gpt-5.4')).toBeInTheDocument();
90
- expect(screen.getByText(/Profile instances/i)).toBeInTheDocument();
102
+ expect(screen.getAllByText('OpenAI').length).toBeGreaterThan(0);
103
+ expect(screen.getAllByText('gpt-5.4').length).toBeGreaterThan(0);
104
+ expect(screen.queryByText(/Profile instances/i)).not.toBeInTheDocument();
105
+ expect(screen.queryByText(/Choose a keeper here if duplicates need to be merged/i)).not.toBeInTheDocument();
91
106
  const text = container.textContent || '';
92
107
  expect(text.indexOf('Chat Apps')).toBeLessThan(text.indexOf('Coding Tools'));
93
108
  expect(text.indexOf('Coding Tools')).toBeLessThan(text.indexOf('Unclassified'));
94
109
  fireEvent.click(screen.getByRole('button', { name: /codex/i }));
95
110
  expect(screen.getAllByText('Terminal-first coding agent.').length).toBeGreaterThan(0);
96
111
  expect(screen.getAllByText('Implementation partner')[0]).toBeInTheDocument();
97
- expect(screen.getByText('OpenAI · gpt-5-codex')).toBeInTheDocument();
112
+ expect(screen.getAllByText('OpenAI').length).toBeGreaterThan(0);
113
+ expect(screen.getAllByText('gpt-5-codex').length).toBeGreaterThan(0);
98
114
  expect(screen.getByText(/2 linked/i)).toBeInTheDocument();
115
+ expect(screen.getByText(/Profile instances/i)).toBeInTheDocument();
116
+ expect(screen.getByText(/Choose a keeper here if duplicates need to be merged/i)).toBeInTheDocument();
99
117
  expect(screen.getAllByTitle('Keep this profile, merge others into it')).toHaveLength(2);
100
118
  expect(screen.getByText('@codex-duplicate')).toBeInTheDocument();
119
+ expect(screen.getAllByTitle('Cloud MCP').length).toBeGreaterThan(0);
120
+ expect(screen.getAllByTitle('Local MCP').length).toBeGreaterThan(0);
121
+ });
122
+ it('removes a profile from the active roster and reloads the list', async () => {
123
+ const confirmMock = vi.fn().mockReturnValue(true);
124
+ const fetchMock = vi.fn()
125
+ .mockResolvedValueOnce({
126
+ ok: true,
127
+ json: async () => ({
128
+ aiProfileSeatUsage: {
129
+ used: 1,
130
+ limit: null,
131
+ remaining: null
132
+ },
133
+ assignees: [
134
+ {
135
+ value: 'ai-codex',
136
+ label: 'Codex',
137
+ username: 'codex',
138
+ icon: 'Bot',
139
+ color: '#0ea5e9',
140
+ kind: 'agent',
141
+ surfaceType: 'coding_tool',
142
+ description: 'Terminal-first coding agent.',
143
+ role: 'Implementation partner',
144
+ provider: 'OpenAI',
145
+ model: 'gpt-5-codex',
146
+ createdAt: '2026-04-02T09:00:00.000Z',
147
+ updatedAt: '2026-04-04T13:15:00.000Z',
148
+ lastActiveAt: '2026-04-04T13:45:00.000Z'
149
+ }
150
+ ]
151
+ })
152
+ })
153
+ .mockResolvedValueOnce({
154
+ ok: true,
155
+ json: async () => ({
156
+ profile: {
157
+ id: 'ai-codex',
158
+ archivedReason: 'manual_archive'
159
+ }
160
+ })
161
+ })
162
+ .mockResolvedValueOnce({
163
+ ok: true,
164
+ json: async () => ({
165
+ aiProfileSeatUsage: {
166
+ used: 0,
167
+ limit: null,
168
+ remaining: null
169
+ },
170
+ assignees: []
171
+ })
172
+ });
173
+ vi.stubGlobal('fetch', fetchMock);
174
+ vi.stubGlobal('confirm', confirmMock);
175
+ render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true, cloudAiProfileSeatUsage: { used: 1, limit: 3, remaining: 2 } }));
176
+ expect(await screen.findByRole('button', { name: /codex/i })).toBeInTheDocument();
177
+ fireEvent.click(screen.getByRole('button', { name: /remove from roster/i }));
178
+ await waitFor(() => expect(confirmMock).toHaveBeenCalledTimes(1));
179
+ expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/taskforce/workspace/ai-profiles/archive', expect.objectContaining({
180
+ method: 'POST',
181
+ credentials: 'include'
182
+ }));
183
+ expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/taskforce/workspace/assignee-options?workspaceId=workspace-1', expect.objectContaining({
184
+ credentials: 'include'
185
+ }));
186
+ expect(await screen.findByText('AI profile removed from active roster.')).toBeInTheDocument();
187
+ expect(await screen.findByText('No AI profiles registered yet.')).toBeInTheDocument();
188
+ });
189
+ it('shows archive permission errors inline above the remove button', async () => {
190
+ const confirmMock = vi.fn().mockReturnValue(true);
191
+ const fetchMock = vi.fn()
192
+ .mockResolvedValueOnce({
193
+ ok: true,
194
+ json: async () => ({
195
+ aiProfileSeatUsage: {
196
+ used: 1,
197
+ limit: null,
198
+ remaining: null
199
+ },
200
+ assignees: [
201
+ {
202
+ value: 'ai-codex',
203
+ label: 'Codex',
204
+ username: 'codex',
205
+ icon: 'Bot',
206
+ color: '#0ea5e9',
207
+ kind: 'agent',
208
+ surfaceType: 'coding_tool',
209
+ description: 'Terminal-first coding agent.',
210
+ role: 'Implementation partner',
211
+ provider: 'OpenAI',
212
+ model: 'gpt-5-codex',
213
+ createdAt: '2026-04-02T09:00:00.000Z',
214
+ updatedAt: '2026-04-04T13:15:00.000Z',
215
+ lastActiveAt: '2026-04-04T13:45:00.000Z'
216
+ }
217
+ ]
218
+ })
219
+ })
220
+ .mockResolvedValueOnce({
221
+ ok: false,
222
+ json: async () => ({
223
+ error: 'Forbidden: owner or admin role required.'
224
+ })
225
+ });
226
+ vi.stubGlobal('fetch', fetchMock);
227
+ vi.stubGlobal('confirm', confirmMock);
228
+ render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true, cloudAiProfileSeatUsage: { used: 1, limit: 3, remaining: 2 } }));
229
+ expect(await screen.findByRole('button', { name: /codex/i })).toBeInTheDocument();
230
+ fireEvent.click(screen.getByRole('button', { name: /remove from roster/i }));
231
+ const inlineError = await screen.findByRole('alert');
232
+ expect(inlineError).toHaveTextContent('Forbidden: owner or admin role required.');
233
+ const removeButton = screen.getByRole('button', { name: /remove from roster/i });
234
+ expect(removeButton.compareDocumentPosition(inlineError) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy();
235
+ });
236
+ it('falls back to local seat usage only when cloud auth is not configured', async () => {
237
+ const fetchMock = vi.fn()
238
+ .mockResolvedValueOnce({
239
+ ok: true,
240
+ json: async () => ({
241
+ aiProfileSeatUsage: {
242
+ used: 1,
243
+ limit: 3,
244
+ remaining: 2
245
+ },
246
+ assignees: [
247
+ {
248
+ value: 'ai-codex',
249
+ label: 'Codex',
250
+ username: 'codex',
251
+ icon: 'Bot',
252
+ color: '#0ea5e9',
253
+ kind: 'agent',
254
+ surfaceType: 'coding_tool',
255
+ seatScope: 'local_unmetered',
256
+ createdAt: '2026-04-02T09:00:00.000Z',
257
+ updatedAt: '2026-04-04T13:15:00.000Z',
258
+ lastActiveAt: '2026-04-04T13:45:00.000Z'
259
+ }
260
+ ]
261
+ })
262
+ });
263
+ vi.stubGlobal('fetch', fetchMock);
264
+ render(_jsx(AgentsModule, { workspaceId: "workspace-1" }));
265
+ expect(await screen.findByText('Registered Cloud Agents: 1/3')).toBeInTheDocument();
266
+ });
267
+ it('shows no cloud seat label when cloud auth is configured but cloud seat usage is unavailable', async () => {
268
+ const fetchMock = vi.fn()
269
+ .mockResolvedValueOnce({
270
+ ok: true,
271
+ json: async () => ({
272
+ aiProfileSeatUsage: {
273
+ used: 0,
274
+ limit: null,
275
+ remaining: null
276
+ },
277
+ assignees: [
278
+ {
279
+ value: 'ai-codex',
280
+ label: 'Codex',
281
+ username: 'codex',
282
+ icon: 'Bot',
283
+ color: '#0ea5e9',
284
+ kind: 'agent',
285
+ surfaceType: 'coding_tool',
286
+ seatScope: 'cloud_metered',
287
+ createdAt: '2026-04-02T09:00:00.000Z',
288
+ updatedAt: '2026-04-04T13:15:00.000Z',
289
+ lastActiveAt: '2026-04-04T13:45:00.000Z'
290
+ }
291
+ ]
292
+ })
293
+ });
294
+ vi.stubGlobal('fetch', fetchMock);
295
+ render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true }));
296
+ await screen.findByRole('button', { name: /codex/i });
297
+ expect(screen.queryByText(/Registered Cloud Agents:/i)).not.toBeInTheDocument();
298
+ expect(screen.queryByText('Registered Cloud Agents: 0/Unlimited')).not.toBeInTheDocument();
299
+ });
300
+ it('shows resolved cloud seat usage when the account summary updates after mount', async () => {
301
+ const fetchMock = vi.fn()
302
+ .mockResolvedValueOnce({
303
+ ok: true,
304
+ json: async () => ({
305
+ aiProfileSeatUsage: {
306
+ used: 0,
307
+ limit: null,
308
+ remaining: null
309
+ },
310
+ assignees: [
311
+ {
312
+ value: 'ai-codex',
313
+ label: 'Codex',
314
+ username: 'codex',
315
+ icon: 'Bot',
316
+ color: '#0ea5e9',
317
+ kind: 'agent',
318
+ surfaceType: 'coding_tool',
319
+ seatScope: 'cloud_metered',
320
+ createdAt: '2026-04-02T09:00:00.000Z',
321
+ updatedAt: '2026-04-04T13:15:00.000Z',
322
+ lastActiveAt: '2026-04-04T13:45:00.000Z'
323
+ }
324
+ ]
325
+ })
326
+ });
327
+ vi.stubGlobal('fetch', fetchMock);
328
+ const { rerender } = render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true, cloudAiProfileSeatUsage: null }));
329
+ await screen.findByRole('button', { name: /codex/i });
330
+ expect(screen.queryByText(/Registered Cloud Agents:/i)).not.toBeInTheDocument();
331
+ rerender(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: true, cloudAiProfileSeatUsage: { used: 0, limit: 20, remaining: 20 } }));
332
+ expect(await screen.findByText('Registered Cloud Agents: 0/20')).toBeInTheDocument();
333
+ });
334
+ it('shows a cloud login prompt instead of unlimited when cloud auth is configured but the user is signed out', async () => {
335
+ const fetchMock = vi.fn()
336
+ .mockResolvedValueOnce({
337
+ ok: true,
338
+ json: async () => ({
339
+ aiProfileSeatUsage: {
340
+ used: 0,
341
+ limit: null,
342
+ remaining: null
343
+ },
344
+ assignees: []
345
+ })
346
+ });
347
+ vi.stubGlobal('fetch', fetchMock);
348
+ render(_jsx(AgentsModule, { workspaceId: "workspace-1", cloudAuthConfigured: true, authSessionResolved: true, isAuthenticated: false }));
349
+ expect(await screen.findByText('Log into account for Cloud agents')).toBeInTheDocument();
350
+ expect(screen.queryByText('Registered Cloud Agents: 0/Unlimited')).not.toBeInTheDocument();
101
351
  });
102
352
  });
@@ -1,4 +1,4 @@
1
- import { type DocMeta } from './DocumentWorkspace';
1
+ import { type DocMeta } from './documentWorkspaceModel';
2
2
  interface Props {
3
3
  documents: DocMeta[];
4
4
  selectedDocId: string | null;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Search, RefreshCw, FileText, ClipboardList, Eye, BookOpen, File } from 'lucide-react';
3
3
  import styles from '../../Taskforce.module.css';
4
4
  import indexStyles from './DocumentIndex.module.css';
5
- import { getEffectiveAttachmentState } from './DocumentWorkspace';
5
+ import { getEffectiveAttachmentState } from './documentWorkspaceModel';
6
6
  import { getReadableDocumentName } from '../../utils/documentNames';
7
7
  function DocTypeIcon({ docType }) {
8
8
  switch (docType) {
@@ -1,13 +1,17 @@
1
- import { type DocMeta } from './DocumentWorkspace';
1
+ import { type DocMeta } from './documentWorkspaceModel';
2
2
  export declare function buildPrintDocumentHtml(markdown: string): string;
3
3
  export declare function schedulePrintCleanup(cleanup: () => void): number;
4
4
  interface Props {
5
5
  doc: DocMeta;
6
6
  taskTitle: string | null;
7
+ tasks?: Array<{
8
+ id: string;
9
+ title: string;
10
+ }>;
7
11
  apiBaseUrl?: string;
8
12
  onSaved?: (updatedAt: string) => void;
9
13
  onDeleted?: () => void;
10
14
  enableTaskGeneration?: boolean;
11
15
  }
12
- export declare function DocumentViewer({ doc, taskTitle, apiBaseUrl, onSaved, onDeleted, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
16
+ export declare function DocumentViewer({ doc, taskTitle, tasks, apiBaseUrl, onSaved, onDeleted, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
13
17
  export {};
@@ -8,7 +8,8 @@ import { Printer, Link, Download, Clock, HardDrive, Tag, Pencil, X, Save, Eye, S
8
8
  import styles from '../../Taskforce.module.css';
9
9
  import viewerStyles from './DocumentViewer.module.css';
10
10
  import taskFormStyles from '../task/TaskForm.module.css';
11
- import { getEffectiveAttachmentState } from './DocumentWorkspace';
11
+ import { Modal } from '../ui/Modal';
12
+ import { getEffectiveAttachmentState } from './documentWorkspaceModel';
12
13
  import { DocumentGenerateModal } from './DocumentGenerateModal';
13
14
  import { fetchTaskforceApi, resolveTaskforceApiUrl } from '../../utils/taskforceApiClient';
14
15
  import { getDocumentDownloadName, getReadableDocumentName } from '../../utils/documentNames';
@@ -500,7 +501,7 @@ function toggleNthTaskCheckbox(markdown, targetIndex, nextChecked) {
500
501
  });
501
502
  return updated.join('\n');
502
503
  }
503
- export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDeleted, enableTaskGeneration = false }) {
504
+ export function DocumentViewer({ doc, taskTitle, tasks = [], apiBaseUrl = '', onSaved, onDeleted, enableTaskGeneration = false }) {
504
505
  const fallbackReadableName = getReadableDocumentName(doc);
505
506
  const [currentDocumentName, setCurrentDocumentName] = useState(fallbackReadableName);
506
507
  const readableName = currentDocumentName;
@@ -522,6 +523,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
522
523
  const [reviewTitleDraft, setReviewTitleDraft] = useState('');
523
524
  const [editingCommentId, setEditingCommentId] = useState(null);
524
525
  const [editingCommentDraft, setEditingCommentDraft] = useState('');
526
+ const [showReviewSidebar, setShowReviewSidebar] = useState(true);
525
527
  // Edit mode state
526
528
  const [editMode, setEditMode] = useState(false);
527
529
  const [draft, setDraft] = useState('');
@@ -531,10 +533,14 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
531
533
  const [monochromeMode, setMonochromeMode] = useState(false);
532
534
  const [deleting, setDeleting] = useState(false);
533
535
  const [renaming, setRenaming] = useState(false);
536
+ const [attachModalOpen, setAttachModalOpen] = useState(false);
537
+ const [attachSearch, setAttachSearch] = useState('');
538
+ const [attachingTaskId, setAttachingTaskId] = useState(null);
539
+ const [attachError, setAttachError] = useState(null);
534
540
  const [renameValue, setRenameValue] = useState(readableName);
535
541
  const isEditable = doc.editable !== false;
536
542
  const attachmentState = getEffectiveAttachmentState(doc);
537
- const canDelete = attachmentState === 'unattached' && !!doc.assetId;
543
+ const canDelete = !!doc.assetId;
538
544
  const isDirty = editMode && draft !== (content ?? '');
539
545
  const textareaRef = useRef(null);
540
546
  const renameInputRef = useRef(null);
@@ -544,6 +550,12 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
544
550
  const reviewThreadEndRef = useRef(null);
545
551
  const markdownContainerRef = useRef(null);
546
552
  const activeReviewSession = reviewSessions.find((session) => session.id === selectedReviewSessionId) || reviewSessions[0] || null;
553
+ const filteredTasks = tasks.filter((task) => {
554
+ const query = attachSearch.trim().toLowerCase();
555
+ if (!query)
556
+ return true;
557
+ return task.title.toLowerCase().includes(query);
558
+ });
547
559
  const fetchContent = useCallback(async () => {
548
560
  setLoadingContent(true);
549
561
  setLoadError(null);
@@ -913,13 +925,20 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
913
925
  const handleDelete = useCallback(async () => {
914
926
  if (!doc.assetId || deleting)
915
927
  return;
916
- if (!window.confirm(`Permanently delete "${readableName}" from storage?`))
928
+ const isAttached = attachmentState === 'attached';
929
+ const linkedTaskCount = Math.max(0, Number(doc.attachmentCount || 0));
930
+ const confirmationMessage = isAttached
931
+ ? `Delete "${readableName}" everywhere?\n\nThis will remove it from ${linkedTaskCount} linked task${linkedTaskCount === 1 ? '' : 's'} and soft-delete the document across sync.`
932
+ : `Permanently delete "${readableName}" from storage?`;
933
+ if (!window.confirm(confirmationMessage))
917
934
  return;
918
935
  setDeleting(true);
919
936
  try {
920
937
  const res = await fetchTaskforceApi(`/api/taskforce/documents/${encodeURIComponent(doc.assetId)}`, {
921
938
  method: 'DELETE',
922
939
  credentials: 'include',
940
+ headers: { 'Content-Type': 'application/json' },
941
+ body: JSON.stringify(isAttached ? { forceDetach: true } : {}),
923
942
  }, apiBaseUrl);
924
943
  const data = await res.json().catch(() => ({}));
925
944
  if (!res.ok)
@@ -932,7 +951,45 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
932
951
  finally {
933
952
  setDeleting(false);
934
953
  }
935
- }, [apiBaseUrl, deleting, doc.assetId, onDeleted, readableName]);
954
+ }, [apiBaseUrl, attachmentState, deleting, doc.assetId, doc.attachmentCount, onDeleted, readableName]);
955
+ const handleOpenAttachModal = useCallback(() => {
956
+ setAttachSearch('');
957
+ setAttachError(null);
958
+ setAttachingTaskId(null);
959
+ setAttachModalOpen(true);
960
+ }, []);
961
+ const handleAttachToTask = useCallback(async (taskId) => {
962
+ if (!doc.assetId)
963
+ return;
964
+ setAttachError(null);
965
+ setAttachingTaskId(taskId);
966
+ try {
967
+ const res = await fetchTaskforceApi(`/api/taskforce/documents/${encodeURIComponent(doc.assetId)}/attach`, {
968
+ method: 'POST',
969
+ credentials: 'include',
970
+ headers: { 'Content-Type': 'application/json' },
971
+ body: JSON.stringify({
972
+ targetType: 'task',
973
+ targetId: taskId,
974
+ }),
975
+ }, apiBaseUrl);
976
+ const data = await res.json().catch(() => ({}));
977
+ if (!res.ok)
978
+ throw new Error(data.error || `Attach failed (${res.status})`);
979
+ if (data.alreadyAttached) {
980
+ setAttachError('This document is already attached to that task.');
981
+ return;
982
+ }
983
+ setAttachModalOpen(false);
984
+ onSaved?.(new Date().toISOString());
985
+ }
986
+ catch (error) {
987
+ setAttachError(error?.message || 'Failed to attach document.');
988
+ }
989
+ finally {
990
+ setAttachingTaskId(null);
991
+ }
992
+ }, [apiBaseUrl, doc.assetId, onSaved]);
936
993
  const handleToggleReviewStatus = useCallback(async () => {
937
994
  if (!activeReviewSession || reviewBusy)
938
995
  return;
@@ -992,7 +1049,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
992
1049
  }, [activeReviewSession, apiBaseUrl, reviewBusy, reviewTitleDraft]);
993
1050
  const previewContent = editMode ? draft : (content ?? '');
994
1051
  const handleCaptureReviewAnchor = useCallback(() => {
995
- if (editMode)
1052
+ if (editMode || !showReviewSidebar)
996
1053
  return;
997
1054
  const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;
998
1055
  const selectedText = selection?.toString().replace(/\s+/g, ' ').trim() || '';
@@ -1011,7 +1068,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1011
1068
  if (!anchor)
1012
1069
  return;
1013
1070
  setPendingReviewAnchor(anchor);
1014
- }, [editMode]);
1071
+ }, [editMode, showReviewSidebar]);
1072
+ const handleToggleReviewSidebar = useCallback(() => {
1073
+ setShowReviewSidebar((current) => {
1074
+ const nextValue = !current;
1075
+ if (!nextValue) {
1076
+ setPendingReviewAnchor(null);
1077
+ }
1078
+ return nextValue;
1079
+ });
1080
+ }, []);
1015
1081
  const handleAddReviewComment = useCallback(async () => {
1016
1082
  const body = reviewDraft.trim();
1017
1083
  if (!body || reviewBusy)
@@ -1210,10 +1276,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1210
1276
  return;
1211
1277
  setRenameValue(readableName);
1212
1278
  setRenaming(true);
1213
- }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: "Permanently delete this unattached document", children: [_jsx(Trash2, { size: 13 }), deleting ? 'Deleting…' : 'Delete Permanently'] }))] })) })] }), loadingContent && (_jsx("div", { className: viewerStyles.viewerState, children: "Loading\u2026" })), !loadingContent && loadError && (_jsx("div", { className: viewerStyles.viewerStateError, children: loadError })), !loadingContent && !loadError && (editMode ? (_jsxs("div", { className: `${viewerStyles.editorArea} ${EDITOR_LAYOUT_CLASS[layout]}`, children: [(layout === 'editor' || layout === 'split') && (_jsxs("div", { className: viewerStyles.editorPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Markdown" }), _jsx("textarea", { ref: textareaRef, className: `${viewerStyles.editorTextarea} tf-scrollbar`, value: draft, onScroll: () => syncPaneScroll('editor'), onChange: e => {
1279
+ }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: handleOpenAttachModal, title: "Attach document to a task", children: [_jsx(Link, { size: 13 }), "Attach"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleToggleReviewSidebar, title: showReviewSidebar ? 'Hide review and comments' : 'Show review and comments', children: [_jsx(MessageSquareText, { size: 13 }), showReviewSidebar ? 'Hide Review' : 'Show Review'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: attachmentState === 'attached'
1280
+ ? 'Remove this document from linked tasks and delete it'
1281
+ : 'Permanently delete this unattached document', children: [_jsx(Trash2, { size: 13 }), deleting
1282
+ ? 'Deleting…'
1283
+ : attachmentState === 'attached'
1284
+ ? 'Delete & Unlink'
1285
+ : 'Delete Permanently'] }))] })) })] }), loadingContent && (_jsx("div", { className: viewerStyles.viewerState, children: "Loading\u2026" })), !loadingContent && loadError && (_jsx("div", { className: viewerStyles.viewerStateError, children: loadError })), !loadingContent && !loadError && (editMode ? (_jsxs("div", { className: `${viewerStyles.editorArea} ${EDITOR_LAYOUT_CLASS[layout]}`, children: [(layout === 'editor' || layout === 'split') && (_jsxs("div", { className: viewerStyles.editorPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Markdown" }), _jsx("textarea", { ref: textareaRef, className: `${viewerStyles.editorTextarea} tf-scrollbar`, value: draft, onScroll: () => syncPaneScroll('editor'), onChange: e => {
1214
1286
  setDraft(e.target.value);
1215
1287
  setSaveStatus(prev => prev.type === 'saving' ? prev : { type: 'idle' });
1216
- }, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: viewerStyles.viewerBody, children: [_jsx("div", { className: `${viewerStyles.viewerContent} ${viewerStyles.viewerMain} tf-scrollbar`, children: _jsx("div", { ref: markdownContainerRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, onMouseUp: handleCaptureReviewAnchor, children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) }) }), _jsx("aside", { className: viewerStyles.reviewRail, children: _jsxs("section", { className: viewerStyles.reviewSection, "aria-label": "Review and comments", children: [_jsxs("div", { className: viewerStyles.reviewHeader, children: [_jsxs("div", { children: [_jsxs("div", { className: viewerStyles.reviewTitle, children: [_jsx(MessageSquareText, { size: 14 }), "Review & Comments"] }), _jsx("p", { className: viewerStyles.reviewSubtitle, children: "Comments stay attached to this document review session." })] }), _jsxs("div", { className: viewerStyles.reviewControls, children: [reviewSessions.length > 1 && (_jsx("select", { className: `${styles.select} ${viewerStyles.reviewSelect}`, value: activeReviewSession?.id || '', onChange: (event) => setSelectedReviewSessionId(event.target.value || null), "aria-label": "Select review session", children: reviewSessions.map((session) => (_jsx("option", { value: session.id, children: session.title?.trim() || 'Untitled review' }, session.id))) })), _jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void createReviewSession(); }, disabled: reviewBusy || !doc.assetId, children: [_jsx(MessageSquareText, { size: 13 }), "Start Review"] }), activeReviewSession && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void handleToggleReviewStatus(); }, disabled: reviewBusy, children: [activeReviewSession.status === 'resolved' ? _jsx(RotateCcw, { size: 13 }) : _jsx(CheckCheck, { size: 13 }), activeReviewSession.status === 'resolved' ? 'Reopen' : 'Resolve'] }))] })] }), activeReviewSession && (_jsxs("div", { className: viewerStyles.reviewSessionMeta, children: [_jsx("span", { className: `${viewerStyles.statusBadge} ${activeReviewSession.status === 'resolved' ? viewerStyles.reviewStatusResolved : ''}`, children: activeReviewSession.status === 'resolved' ? 'Resolved' : 'Open' }), editingReviewTitle ? (_jsxs("div", { className: viewerStyles.reviewTitleEditRow, children: [_jsx("input", { type: "text", className: styles.input, value: reviewTitleDraft, onChange: (event) => setReviewTitleDraft(event.target.value), placeholder: "Untitled review" }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => { void handleSaveReviewTitle(); }, disabled: reviewBusy, children: "Save Title" })] })) : (_jsx("span", { className: viewerStyles.reviewSessionLabel, children: activeReviewSession.title?.trim() || 'Untitled review' })), _jsxs("span", { className: viewerStyles.reviewSessionTimestamp, children: ["Updated ", formatReviewTimestamp(activeReviewSession.updatedAt)] }), !editingReviewTitle && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => setEditingReviewTitle(true), disabled: reviewBusy, children: [_jsx(Pencil, { size: 13 }), "Rename Review"] }))] })), reviewError && (_jsx("div", { className: viewerStyles.viewerStateError, children: reviewError })), _jsxs("div", { className: taskFormStyles.commentPanel, children: [_jsxs("div", { className: taskFormStyles.commentThread, children: [loadingReviews && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Loading review comments\u2026" })), !loadingReviews && !activeReviewSession && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Start a review to collect comments for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.length === 0 && (_jsx("div", { className: taskFormStyles.emptyComments, children: "No comments yet. Add the first review note for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.map((comment) => {
1288
+ }, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: `${viewerStyles.viewerBody} ${!showReviewSidebar ? viewerStyles.viewerBodyFullWidth : ''}`, children: [_jsx("div", { className: `${viewerStyles.viewerContent} ${viewerStyles.viewerMain} tf-scrollbar`, children: _jsx("div", { ref: markdownContainerRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, onMouseUp: showReviewSidebar ? handleCaptureReviewAnchor : undefined, children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) }) }), showReviewSidebar && (_jsx("aside", { className: viewerStyles.reviewRail, children: _jsxs("section", { className: viewerStyles.reviewSection, "aria-label": "Review and comments", children: [_jsxs("div", { className: viewerStyles.reviewHeader, children: [_jsxs("div", { children: [_jsxs("div", { className: viewerStyles.reviewTitle, children: [_jsx(MessageSquareText, { size: 14 }), "Review & Comments"] }), _jsx("p", { className: viewerStyles.reviewSubtitle, children: "Comments stay attached to this document review session." })] }), _jsxs("div", { className: viewerStyles.reviewControls, children: [reviewSessions.length > 1 && (_jsx("select", { className: `${styles.select} ${viewerStyles.reviewSelect}`, value: activeReviewSession?.id || '', onChange: (event) => setSelectedReviewSessionId(event.target.value || null), "aria-label": "Select review session", children: reviewSessions.map((session) => (_jsx("option", { value: session.id, children: session.title?.trim() || 'Untitled review' }, session.id))) })), _jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void createReviewSession(); }, disabled: reviewBusy || !doc.assetId, children: [_jsx(MessageSquareText, { size: 13 }), "Start Review"] }), activeReviewSession && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void handleToggleReviewStatus(); }, disabled: reviewBusy, children: [activeReviewSession.status === 'resolved' ? _jsx(RotateCcw, { size: 13 }) : _jsx(CheckCheck, { size: 13 }), activeReviewSession.status === 'resolved' ? 'Reopen' : 'Resolve'] }))] })] }), activeReviewSession && (_jsxs("div", { className: viewerStyles.reviewSessionMeta, children: [_jsx("span", { className: `${viewerStyles.statusBadge} ${activeReviewSession.status === 'resolved' ? viewerStyles.reviewStatusResolved : ''}`, children: activeReviewSession.status === 'resolved' ? 'Resolved' : 'Open' }), editingReviewTitle ? (_jsxs("div", { className: viewerStyles.reviewTitleEditRow, children: [_jsx("input", { type: "text", className: styles.input, value: reviewTitleDraft, onChange: (event) => setReviewTitleDraft(event.target.value), placeholder: "Untitled review" }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => { void handleSaveReviewTitle(); }, disabled: reviewBusy, children: "Save Title" })] })) : (_jsx("span", { className: viewerStyles.reviewSessionLabel, children: activeReviewSession.title?.trim() || 'Untitled review' })), _jsxs("span", { className: viewerStyles.reviewSessionTimestamp, children: ["Updated ", formatReviewTimestamp(activeReviewSession.updatedAt)] }), !editingReviewTitle && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => setEditingReviewTitle(true), disabled: reviewBusy, children: [_jsx(Pencil, { size: 13 }), "Rename Review"] }))] })), reviewError && (_jsx("div", { className: viewerStyles.viewerStateError, children: reviewError })), _jsxs("div", { className: taskFormStyles.commentPanel, children: [_jsxs("div", { className: taskFormStyles.commentThread, children: [loadingReviews && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Loading review comments\u2026" })), !loadingReviews && !activeReviewSession && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Start a review to collect comments for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.length === 0 && (_jsx("div", { className: taskFormStyles.emptyComments, children: "No comments yet. Add the first review note for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.map((comment) => {
1217
1289
  const isAi = comment.authorActor?.kind === 'ai';
1218
1290
  const anchorLabel = formatReviewAnchor(comment.anchor);
1219
1291
  const ActorIcon = isAi ? Bot : User;
@@ -1231,5 +1303,13 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1231
1303
  event.preventDefault();
1232
1304
  void handleAddReviewComment();
1233
1305
  }
1234
- } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) })] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1306
+ } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) }))] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), _jsx(Modal, { isOpen: attachModalOpen, onClose: () => {
1307
+ if (attachingTaskId)
1308
+ return;
1309
+ setAttachModalOpen(false);
1310
+ }, title: "Attach document", size: "md", footer: (_jsx("button", { className: viewerStyles.actionBtn, onClick: () => setAttachModalOpen(false), disabled: Boolean(attachingTaskId), children: "Close" })), children: _jsxs("div", { className: viewerStyles.attachModalBody, children: [_jsx("p", { className: viewerStyles.attachModalCopy, children: "Attach this document to a task from the document manager. Initiative and workstream targets can be enabled later on the same attachment model." }), _jsx("input", { type: "text", className: styles.input, placeholder: "Search tasks\u2026", value: attachSearch, onChange: (event) => setAttachSearch(event.target.value) }), attachError && (_jsxs("div", { className: viewerStyles.attachModalError, children: [_jsx(AlertCircle, { size: 14 }), _jsx("span", { children: attachError })] })), _jsx("div", { className: `${viewerStyles.attachTaskList} tf-scrollbar`, children: filteredTasks.length === 0 ? (_jsx("div", { className: viewerStyles.attachEmptyState, children: tasks.length === 0 ? 'No tasks are available in this workspace.' : 'No tasks match your search.' })) : (filteredTasks.map((task) => {
1311
+ const isCurrentPrimaryAttachment = doc.taskId === task.id;
1312
+ const isBusy = attachingTaskId === task.id;
1313
+ return (_jsxs("button", { type: "button", className: viewerStyles.attachTaskRow, disabled: isBusy, onClick: () => { void handleAttachToTask(task.id); }, children: [_jsx("span", { className: viewerStyles.attachTaskTitle, children: task.title }), _jsx("span", { className: viewerStyles.attachTaskMeta, children: isBusy ? 'Attaching…' : isCurrentPrimaryAttachment ? 'Attached' : 'Attach' })] }, task.id));
1314
+ })) })] }) }), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1235
1315
  }
@@ -1,24 +1,8 @@
1
1
  import { type DocumentAttachmentFilter, type DocumentTypeFilter } from './documentWorkspaceFilters';
2
- export interface DocMeta {
3
- id: string;
4
- assetId?: string;
5
- referenceNumber?: number | null;
6
- referenceLabel?: string | null;
7
- fsPath: string;
8
- apiUrl: string;
9
- taskId: string | null;
10
- displayName: string;
11
- originalFilename?: string | null;
12
- title: string;
13
- docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
14
- sizeBytes: number;
15
- updatedAt: string | null;
16
- editable?: boolean;
17
- attachmentCount?: number;
18
- attachmentState?: 'attached' | 'unattached' | 'standalone';
19
- }
2
+ import type { DocMeta } from './documentWorkspaceModel';
20
3
  export type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
21
4
  export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
5
+ export type { DocMeta } from './documentWorkspaceModel';
22
6
  interface Props {
23
7
  tasks: Array<{
24
8
  id: string;
@@ -30,11 +14,11 @@ interface Props {
30
14
  error: string | null;
31
15
  selectedDoc: DocMeta | null;
32
16
  searchQuery: string;
33
- typeFilters: DocumentTypeFilter[];
34
- attachmentFilters: DocumentAttachmentFilter[];
17
+ hasActiveFilters: boolean;
35
18
  onSearchChange: (value: string) => void;
36
19
  onSelectDoc: (doc: DocMeta | null) => void;
37
20
  onRefresh: () => Promise<void> | void;
21
+ onDeleteSelected: () => Promise<void> | void;
38
22
  enableTaskGeneration?: boolean;
39
23
  }
40
24
  interface ShellProps {
@@ -54,6 +38,5 @@ interface ShellProps {
54
38
  onRequestedDocHandled?: () => void;
55
39
  enableTaskGeneration?: boolean;
56
40
  }
57
- export declare function getEffectiveAttachmentState(doc: Pick<DocMeta, 'attachmentState' | 'attachmentCount' | 'taskId'>): 'attached' | 'unattached';
58
- export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
41
+ export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
59
42
  export declare function DocumentWorkspaceShell({ tasks, runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, enableTaskGeneration }: ShellProps): import("react/jsx-runtime").JSX.Element;